191 lines
6.0 KiB
JavaScript
Executable File
191 lines
6.0 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
|
|
import fs from 'node:fs/promises'
|
|
import path from 'node:path'
|
|
import process from 'node:process'
|
|
import {execFileSync} from 'node:child_process'
|
|
import {createRequire} from 'node:module'
|
|
|
|
const require = createRequire(import.meta.url)
|
|
const root = path.resolve(import.meta.dirname, '../..')
|
|
|
|
function fail(message) {
|
|
throw new Error(message)
|
|
}
|
|
|
|
function parseArgs(argv) {
|
|
const result = {}
|
|
for (let i = 0; i < argv.length; i += 2) {
|
|
const key = argv[i]
|
|
const value = argv[i + 1]
|
|
if (!key?.startsWith('--') || value == null)
|
|
fail(`Invalid argument: ${key}`)
|
|
result[key.slice(2)] = value
|
|
}
|
|
return result
|
|
}
|
|
|
|
async function assertFile(relativePath) {
|
|
const absolutePath = path.join(root, relativePath)
|
|
const stat = await fs.stat(absolutePath).catch(() => null)
|
|
if (!stat?.isFile())
|
|
fail(`Required fingerprint input is missing: ${relativePath}`)
|
|
}
|
|
|
|
async function main() {
|
|
const args = parseArgs(process.argv.slice(2))
|
|
const platform = args.platform
|
|
const nativeProfile = args.profile
|
|
if (!['ios', 'android'].includes(platform))
|
|
fail('--platform must be ios or android')
|
|
if (!['production', 'testflight'].includes(nativeProfile)) {
|
|
fail('--profile must be production or testflight')
|
|
}
|
|
|
|
if (
|
|
process.env.EAS_BUILD_PLATFORM &&
|
|
process.env.EAS_BUILD_PLATFORM !== platform
|
|
) {
|
|
fail(
|
|
`EAS_BUILD_PLATFORM=${process.env.EAS_BUILD_PLATFORM} conflicts with ${platform}`,
|
|
)
|
|
}
|
|
if (
|
|
process.env.EXPO_PUBLIC_ENV &&
|
|
process.env.EXPO_PUBLIC_ENV !== nativeProfile
|
|
) {
|
|
fail(
|
|
`EXPO_PUBLIC_ENV=${process.env.EXPO_PUBLIC_ENV} conflicts with ${nativeProfile}`,
|
|
)
|
|
}
|
|
process.env.EAS_BUILD_PLATFORM = platform
|
|
process.env.EXPO_PUBLIC_ENV = nativeProfile
|
|
process.env.OTA_FINGERPRINT_PIPELINE_ENABLED = '1'
|
|
|
|
const sourceCommit = args['source-commit'] ?? process.env.GITHUB_SHA
|
|
if (!/^[0-9a-f]{40}$/.test(sourceCommit ?? ''))
|
|
fail('--source-commit or GITHUB_SHA must be a full lowercase SHA')
|
|
const checkoutCommit = execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
cwd: root,
|
|
encoding: 'utf8',
|
|
}).trim()
|
|
if (sourceCommit !== checkoutCommit)
|
|
fail(
|
|
`sourceCommit ${sourceCommit} does not match checkout HEAD ${checkoutCommit}`,
|
|
)
|
|
|
|
for (const file of [
|
|
'app.config.js',
|
|
'fingerprint.config.js',
|
|
'scripts/ota/fingerprint-policy.json',
|
|
...(platform === 'android' ? ['google-services.json'] : []),
|
|
])
|
|
await assertFile(file)
|
|
if (platform === 'ios') {
|
|
for (const name of ['BlueskyClip', 'BlueskyNSE', 'Share-with-Bluesky']) {
|
|
const stat = await fs
|
|
.stat(path.join(root, 'modules', name))
|
|
.catch(() => null)
|
|
if (!stat?.isDirectory())
|
|
fail(`Required native extension is missing: modules/${name}`)
|
|
}
|
|
}
|
|
|
|
delete require.cache[
|
|
require.resolve(path.join(root, 'fingerprint.config.js'))
|
|
]
|
|
const fingerprintConfig = require(path.join(root, 'fingerprint.config.js'))
|
|
if (
|
|
fingerprintConfig.hashAlgorithm !== 'sha1' ||
|
|
!Array.isArray(fingerprintConfig.extraSources)
|
|
) {
|
|
fail('fingerprint.config.js did not load the canonical policy')
|
|
}
|
|
const {getConfig} = require('expo/config')
|
|
const {exp} = getConfig(root, {
|
|
isPublicConfig: true,
|
|
skipSDKVersionRequirement: true,
|
|
})
|
|
if (exp.runtimeVersion?.policy !== 'fingerprint')
|
|
fail('Expo runtimeVersion policy must be fingerprint')
|
|
|
|
const {
|
|
resolveRuntimeVersionAsync,
|
|
} = require('expo-updates/utils/build/resolveRuntimeVersionAsync')
|
|
const resolved = await resolveRuntimeVersionAsync(
|
|
root,
|
|
platform,
|
|
{silent: true},
|
|
{workflowOverride: 'managed'},
|
|
)
|
|
if (!/^[0-9a-f]{40}$/.test(resolved.runtimeVersion ?? ''))
|
|
fail('Resolver returned an invalid fingerprint runtime')
|
|
if (
|
|
!Array.isArray(resolved.fingerprintSources) ||
|
|
resolved.fingerprintSources.length === 0
|
|
) {
|
|
fail('Resolver returned no fingerprint sources')
|
|
}
|
|
const policySource = resolved.fingerprintSources.find(
|
|
source =>
|
|
source.type === 'file' &&
|
|
source.filePath?.endsWith('fingerprint-policy.json'),
|
|
)
|
|
if (!policySource?.hash)
|
|
fail('Fingerprint report omitted the compatibility policy source')
|
|
const expoConfigSource = resolved.fingerprintSources.find(
|
|
source => source.type === 'contents' && source.id === 'expoConfig',
|
|
)
|
|
if (!expoConfigSource?.hash)
|
|
fail('Fingerprint report omitted resolved Expo config')
|
|
if (platform === 'ios') {
|
|
for (const name of ['BlueskyClip', 'BlueskyNSE', 'Share-with-Bluesky']) {
|
|
const source = resolved.fingerprintSources.find(
|
|
candidate =>
|
|
candidate.type === 'dir' && candidate.filePath === `modules/${name}`,
|
|
)
|
|
if (!source?.hash) fail(`Fingerprint report omitted modules/${name}`)
|
|
}
|
|
} else {
|
|
const googleServicesSource = resolved.fingerprintSources.find(
|
|
source =>
|
|
source.type === 'file' && source.filePath === 'google-services.json',
|
|
)
|
|
if (!googleServicesSource?.hash)
|
|
fail('Fingerprint report omitted google-services.json')
|
|
}
|
|
|
|
const projectPackage = require(path.join(root, 'package.json'))
|
|
const packageManager =
|
|
projectPackage.packageManager ??
|
|
`${projectPackage.devEngines.packageManager.name}@${projectPackage.devEngines.packageManager.version}`
|
|
const report = {
|
|
schemaVersion: 1,
|
|
platform,
|
|
nativeProfile,
|
|
sourceCommit,
|
|
runtimeVersion: resolved.runtimeVersion,
|
|
fingerprintPolicyVersion: 1,
|
|
fingerprintToolVersion: require('@expo/fingerprint/package.json').version,
|
|
toolVersions: {
|
|
expo: require('expo/package.json').version,
|
|
expoUpdates: require('expo-updates/package.json').version,
|
|
node: process.version,
|
|
packageManager,
|
|
},
|
|
fingerprintSources: resolved.fingerprintSources,
|
|
}
|
|
const json = `${JSON.stringify(report, null, 2)}\n`
|
|
if (args.output) {
|
|
await fs.mkdir(path.dirname(path.resolve(args.output)), {recursive: true})
|
|
await fs.writeFile(path.resolve(args.output), json)
|
|
} else {
|
|
process.stdout.write(json)
|
|
}
|
|
}
|
|
|
|
main().catch(error => {
|
|
console.error(error instanceof Error ? error.message : error)
|
|
process.exitCode = 1
|
|
})
|