add canonical fingerprint ota export tooling
This commit is contained in:
Executable
+190
@@ -0,0 +1,190 @@
|
||||
#!/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
|
||||
})
|
||||
Reference in New Issue
Block a user