apple build: --derived-data (default <out>/DerivedData with --out), --path-free (Swift/clang prefix maps, no serialized debug options) and an archive path scan
This commit is contained in:
parent
cb20912f0e
commit
f109c519ae
10 changed files with 276 additions and 7 deletions
|
|
@ -3,6 +3,7 @@
|
|||
// (Mac App Store build of a Tauri app: cli/apple/build-mas.ts)
|
||||
// storebound apple build [--version 1.2.0] [--build-number N] [--scheme S] [--workspace W | --project P]
|
||||
// [--configuration Release] [--out build/ios] [--no-prebuild] [--allow-stale-web] [--dry] [--json]
|
||||
// [--derived-data <dir>] [--path-free] [--scan-paths]
|
||||
// [--keychain <path>] [--keychain-unlock] [--allow-ssh] [--allow-app-group-gap]
|
||||
// Over SSH (no GUI session) codesign needs an unlocked keychain: --keychain-unlock unlocks it with the password in
|
||||
// STOREBOUND_KEYCHAIN_PASSWORD (never printed) and sets its key partition list; --keychain <path> signs with a build
|
||||
|
|
@ -16,15 +17,21 @@
|
|||
// profiles from apple.signing), runs xcodebuild archive and -exportArchive with the API key
|
||||
// (-allowProvisioningUpdates -authenticationKey*), and leaves <out>/export/<version>-<build>/<App>.ipa for `storebound apple upload`. Each build exports into its own
|
||||
// folder, cleaned first: xcodebuild refuses to overwrite an earlier App.ipa ("exportArchive Copy failed", pitfall 53).
|
||||
// DerivedData: --derived-data <dir> (apple.build.derivedData) is xcodebuild -derivedDataPath; with --out (or apple.build.out)
|
||||
// it defaults to <out>/DerivedData, so a build is hermetic per output folder (without --out Xcode keeps ~/Library/...).
|
||||
// --path-free (apple.build.pathFree) remaps the home folder, checkout, out and DerivedData out of the binary and the dSYM /
|
||||
// Symbols (Swift -debug-prefix-map / -file-prefix-map, clang -ffile-prefix-map), then scans the archive and stops before the
|
||||
// export when a home path is left; --scan-paths only scans and warns. A --out outside the home folder prints a hint.
|
||||
// Full xcodebuild output goes to <out>/xcodebuild-*.log; the terminal gets the result lines.
|
||||
// Local only: nothing is sent to App Store Connect except reads (build numbers, team id, profiles).
|
||||
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { homedir } from 'node:os';
|
||||
import { homedir, userInfo } from 'node:os';
|
||||
import { dirname, join, relative } from 'node:path';
|
||||
import { ascKeyFile } from '../lib/credentials';
|
||||
import type { Ctx } from '../lib/ctx';
|
||||
import { need } from '../lib/ctx';
|
||||
import { done, log, printJson, warn } from '../lib/log';
|
||||
import { derivedDataFor, archiveScanDirs, leakNeedles, leakProblem, outsideHome, pathFreeSettings, scanForPaths } from '../lib/path-free';
|
||||
import { pump } from '../lib/runner';
|
||||
import { parseMobileprovision, toXmlPlist } from '../lib/plist';
|
||||
import { nextBuildNumber } from '../lib/release';
|
||||
|
|
@ -148,6 +155,14 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
if (sub) target.notes.unshift(`Capacitor project: ${sub} (${sub}/${cap!.config}; the only one found; set apple.build.workspace to use another)`);
|
||||
const configuration = a.str('configuration', cfg.configuration ?? 'Release');
|
||||
const out = resolve(a.get('out') ?? cfg.out ?? 'build/ios');
|
||||
const outChosen = !!(a.get('out') ?? cfg.out);
|
||||
const derivedData = derivedDataFor({ flag: a.get('derived-data'), config: cfg.derivedData, out, outChosen, resolve });
|
||||
const pathFree = a.has('path-free') || cfg.pathFree === true;
|
||||
const scanPaths = pathFree || a.has('scan-paths');
|
||||
const home = homedir();
|
||||
const pfSettings = pathFree ? pathFreeSettings({ home, root, out, derivedData }) : [];
|
||||
if (!pathFree && outsideHome(out, home)) warn(TAG, `--out is outside the home folder, but the build embeds the home folder and checkout paths (binary, dSYM, Symbols): add --path-free (or apple.build.pathFree) to remap them and scan the archive`);
|
||||
if (pathFree && (!derivedData || !outsideHome(derivedData, home))) warn(TAG, `--path-free: DerivedData ${derivedData ? 'is' : 'defaults to'} inside the home folder, and dsymutil / Xcode record that path in the dSYM, so the scan below will find it. Build with --out (or --derived-data) outside the home folder, e.g. a shared folder.`);
|
||||
const version = a.get('version');
|
||||
if (version && !/^\d+(\.\d+){0,2}$/.test(version)) throw new Error('--version: x.y.z');
|
||||
|
||||
|
|
@ -212,12 +227,12 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
const auth = style === 'automatic' ? authPreview : null;
|
||||
const steps = [
|
||||
...(target.framework === 'flutter' ? [showCmd(flutterConfigArgs(buildNo, version), null)] : []),
|
||||
showCmd(archiveArgs({ target, root, configuration, archivePath, version, build: buildNo, teamId: teamId ?? undefined, auth, settings: keychain ? keychainSigningArgs(keychain) : undefined }), auth),
|
||||
showCmd(archiveArgs({ target, root, configuration, archivePath, version, build: buildNo, teamId: teamId ?? undefined, auth, settings: [...(keychain ? keychainSigningArgs(keychain) : []), ...pfSettings], derivedData }), auth),
|
||||
showCmd(exportArgs(archivePath, join(out, 'ExportOptions.plist'), exportPath, auth), auth),
|
||||
];
|
||||
const unlockPreview = unlock ? unlockSteps(keychain ?? '<login keychain>', []).map(showSecurity) : [];
|
||||
const plan = { framework: target.framework, workspace: target.workspace ?? null, project: target.project ?? null, scheme: target.scheme, configuration,
|
||||
version: version ?? '(project value)', build: buildNo, teamId, signingStyle: style, exportOptions: opts, out: relative(root, out) || '.', steps: [...unlockPreview, ...steps], notes: target.notes };
|
||||
version: version ?? '(project value)', build: buildNo, derivedData: derivedData ?? '(Xcode default)', pathFree, teamId, signingStyle: style, exportOptions: opts, out: relative(root, out) || '.', steps: [...unlockPreview, ...steps], notes: target.notes };
|
||||
const webOpts = { config, root, cap, store: 'apple' as const, tag: TAG, noPrebuild: a.has('no-prebuild'), allowStale: a.has('allow-stale-web') };
|
||||
if (dry) {
|
||||
const web = await webStep({ ...webOpts, dry: true, quiet: json });
|
||||
|
|
@ -245,13 +260,21 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
if (rsync) log(TAG, `note: ${rsync} is first on your PATH; xcodebuild runs with /usr/bin first (a non-Apple rsync breaks Xcode's IPA export)`);
|
||||
mkdirSync(out, { recursive: true });
|
||||
writeFileSync(join(out, 'ExportOptions.plist'), plist);
|
||||
let leaks = 0;
|
||||
const key = style === 'automatic' ? ascKeyFile(root) : null;
|
||||
const realAuth: AscAuth | null = key ? { keyPath: key.keyPath, keyId: key.keyId, issuerId: key.issuerId } : null;
|
||||
try {
|
||||
if (target.framework === 'flutter') await runLogged(flutterConfigArgs(build!, version), root, join(out, 'flutter-config.log'), showCmd(flutterConfigArgs(build!, version), null));
|
||||
shipPhase('build', `xcodebuild archive, build ${build}`);
|
||||
const arch = archiveArgs({ target, root, configuration, archivePath, version, build: build!, teamId: teamId ?? undefined, auth: realAuth, settings: keychain ? keychainSigningArgs(keychain) : undefined });
|
||||
const arch = archiveArgs({ target, root, configuration, archivePath, version, build: build!, teamId: teamId ?? undefined, auth: realAuth, settings: [...(keychain ? keychainSigningArgs(keychain) : []), ...pfSettings], derivedData });
|
||||
await runLogged(arch, root, join(out, 'xcodebuild-archive.log'), showCmd(arch, realAuth));
|
||||
if (scanPaths) {
|
||||
shipPhase('verify', 'scanning the archive for local paths');
|
||||
const found = scanForPaths(archiveScanDirs(archivePath), leakNeedles({ home, user: userInfo().username, root }));
|
||||
leaks = found.total;
|
||||
const why = leakProblem(found, relative(root, archivePath));
|
||||
if (why) { if (pathFree) throw new Error(why); warn(TAG, why.split(String.fromCharCode(10))[0]!); } else log(TAG, 'no local path in the archive (app, dSYMs, Symbols)');
|
||||
}
|
||||
shipPhase('sign', 'xcodebuild -exportArchive (signing)');
|
||||
await exportIpa({ root, out, archivePath, exportPath, auth: realAuth });
|
||||
} finally { key?.cleanup(); }
|
||||
|
|
@ -265,7 +288,7 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
// ITMS-91064 (Invalid Binary after the upload): NSPrivacyTracking / NSPrivacyTrackingDomains of every manifest in the IPA
|
||||
const privacy = bundleTrackingIssues(ipaManifests(join(exportPath, ipa)));
|
||||
if (privacy.length) throw new Error(`${relative(root, join(exportPath, ipa))} was built, but Apple would refuse it at upload:\n${privacy.map(x => ` ${x}`).join('\n')}\nFix the manifest and build again.`);
|
||||
const result = { ipa: join(exportPath, ipa), bundleId: info.bundleId, version: info.version, build: info.build, web: web.bundle };
|
||||
const result = { ipa: join(exportPath, ipa), bundleId: info.bundleId, version: info.version, build: info.build, web: web.bundle, derivedData: derivedData ?? null, pathFree, ...(scanPaths ? { leaks } : {}) };
|
||||
writeFileSync(join(out, 'latest.json'), `${JSON.stringify(result, null, 2)}
|
||||
`);
|
||||
if (json) printJson(result);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue