Windows builds and moved credentials: play build runs gradlew.bat through cmd /c and takes C:/ keystore paths as absolute (pitfall 98); check names credential paths missing on this machine and a keystore the stored password / alias do not open (keytool) (pitfall 99); play / apple build write <out>/latest.json for CI; CI install from a git clone documented
This commit is contained in:
parent
4fcee60ba6
commit
73d308164d
9 changed files with 95 additions and 9 deletions
|
|
@ -26,6 +26,7 @@
|
|||
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { homedir, tmpdir } from 'node:os';
|
||||
import { isAbsolute, join, resolve } from 'node:path';
|
||||
import * as rt from './rt';
|
||||
|
||||
export interface ServiceAccount { client_email: string; private_key: string; token_uri?: string }
|
||||
export interface AscKey { keyId: string; issuerId: string; pem: string }
|
||||
|
|
@ -324,7 +325,7 @@ export function androidSigning(root: string): AndroidSigning | null {
|
|||
const storePassword = value(root, 'ANDROID_KEYSTORE_PASSWORD'), keyAlias = value(root, 'ANDROID_KEY_ALIAS');
|
||||
if (!storePassword || !keyAlias) throw new Error('ANDROID_KEYSTORE is set but ANDROID_KEYSTORE_PASSWORD / ANDROID_KEY_ALIAS are not (env or .storebound/credentials.json)');
|
||||
const abs = storeFile.replace(/^~(?=\/)/, homedir());
|
||||
return { storeFile: abs.startsWith('/') ? abs : join(root, abs), storePassword, keyAlias, keyPassword: value(root, 'ANDROID_KEY_PASSWORD') ?? storePassword };
|
||||
return { storeFile: isAbsolute(abs) ? abs : join(root, abs), storePassword, keyAlias, keyPassword: value(root, 'ANDROID_KEY_PASSWORD') ?? storePassword };
|
||||
}
|
||||
|
||||
/** App Store Connect vendor number for sales and finance reports (Payments and Financial Reports, top left): the
|
||||
|
|
@ -334,3 +335,33 @@ export function ascVendorNumber(root: string): string | null {
|
|||
const v = value(root, 'ASC_VENDOR_NUMBER');
|
||||
return v && /^\d{5,12}$/.test(v.trim()) ? v.trim() : null;
|
||||
}
|
||||
|
||||
/** Credential paths that do not exist (a machine move left another computer's absolute paths) and an upload keystore
|
||||
* the stored password / alias do not open: each names the field. `keytool` runs only when it is on PATH. */
|
||||
export function credentialFileIssues(root: string, run: (argv: string[]) => { ok: boolean; out: string } | null = keytool): string[] {
|
||||
const issues: string[] = [];
|
||||
const asPath = (v: string) => { const p = v.replace(/^~(?=[/\\])/, homedir()); return isAbsolute(p) ? p : join(root, p); };
|
||||
for (const k of ['PLAY_SERVICE_ACCOUNT', 'ASC_P8', 'ANDROID_KEYSTORE'] as const) {
|
||||
let v: string | undefined;
|
||||
try { v = value(root, k); } catch { continue; }
|
||||
if (!v || /^\s*\{/.test(v) || /BEGIN [A-Z ]*PRIVATE KEY/.test(v)) continue;
|
||||
if (!existsSync(asPath(v))) issues.push(`${k}: ${v} does not exist on this machine (moved from another computer?): point it at the file here`);
|
||||
}
|
||||
let s: AndroidSigning | null = null;
|
||||
try { s = androidSigning(root); } catch (e) { issues.push((e as Error).message); }
|
||||
if (s && existsSync(s.storeFile)) {
|
||||
const r = run(['-list', '-keystore', s.storeFile, '-storepass', s.storePassword, '-alias', s.keyAlias]);
|
||||
if (r && !r.ok) issues.push(/password|tampered|Keystore was tampered/i.test(r.out)
|
||||
? 'ANDROID_KEYSTORE_PASSWORD does not open the upload keystore (keytool -list failed): check the password (android/keystore.properties may hold the right one)'
|
||||
: /alias|does not exist/i.test(r.out) ? `ANDROID_KEY_ALIAS "${s.keyAlias}" is not in the upload keystore` : `the upload keystore does not open with the stored password / alias: ${r.out.trim().split('\n')[0]}`);
|
||||
}
|
||||
return issues;
|
||||
}
|
||||
|
||||
function keytool(argv: string[]): { ok: boolean; out: string } | null {
|
||||
const bin = process.env.JAVA_HOME ? join(process.env.JAVA_HOME, 'bin', process.platform === 'win32' ? 'keytool.exe' : 'keytool') : 'keytool';
|
||||
try {
|
||||
const r = rt.spawnSync([bin, ...argv], { timeout: 20_000 });
|
||||
return { ok: r.success, out: `${r.stdout.toString()}${r.stderr.toString()}` };
|
||||
} catch { return null; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ export function pickGradle(root: string, cfg: PlayBuildConfig = {}): GradleTarge
|
|||
if (fw === 'capacitor') notes.push('Capacitor: run npx cap sync android after the web build so the bundle has the current web assets');
|
||||
if (fw === 'unity') notes.push('Unity: export the Gradle project from Unity (Build Settings > Android > Export Project) before each build');
|
||||
if (fw === 'flutter') notes.push('Flutter: built with flutter build appbundle (build number = versionCode)');
|
||||
if (!existsSync(join(dir, process.platform === 'win32' ? 'gradlew.bat' : 'gradlew'))) notes.push('no Gradle wrapper (gradlew) in the project: using gradle from PATH');
|
||||
if (!existsSync(join(dir, 'gradlew')) && !existsSync(join(dir, 'gradlew.bat'))) notes.push('no Gradle wrapper (gradlew) in the project: using gradle from PATH');
|
||||
return { framework: fw, dir, module, task: `:${module}:bundle${flavor}Release`, notes };
|
||||
}
|
||||
|
||||
|
|
@ -57,10 +57,11 @@ export function pickGradle(root: string, cfg: PlayBuildConfig = {}): GradleTarge
|
|||
* override defaultConfig for bundleRelease: checked with AGP 8.13 (2026-10-12), `-Pandroid.injected.version.code=42`
|
||||
* on a build file with `versionCode 1` gives an AAB with versionCode 1; seen live the same day with a Capacitor app.
|
||||
* They stay on the command line for older plugins / IDE parity only; the AAB check after the build is the stop. */
|
||||
export function gradleArgs(t: GradleTarget, o: { versionCode?: number; versionName?: string; flavor?: string; hasWrapper: boolean }): string[] {
|
||||
export function gradleArgs(t: GradleTarget, o: { versionCode?: number; versionName?: string; flavor?: string; hasWrapper: boolean }, platform: string = process.platform): string[] {
|
||||
if (t.framework === 'flutter')
|
||||
return ['flutter', 'build', 'appbundle', '--release', ...(o.flavor ? ['--flavor', o.flavor] : []), ...(o.versionCode ? ['--build-number', String(o.versionCode)] : []), ...(o.versionName ? ['--build-name', o.versionName] : [])];
|
||||
return [o.hasWrapper ? (process.platform === 'win32' ? 'gradlew.bat' : './gradlew') : 'gradle', t.task, '--no-daemon', '--console=plain',
|
||||
// Windows: a bare `gradlew.bat` is searched on PATH only ("Executable not found in $PATH"); cmd /c finds it in the cwd
|
||||
return [...(o.hasWrapper ? (platform === 'win32' ? ['cmd', '/c', 'gradlew.bat'] : ['./gradlew']) : platform === 'win32' ? ['cmd', '/c', 'gradle'] : ['gradle']), t.task, '--no-daemon', '--console=plain',
|
||||
...(o.versionCode ? [`-Pandroid.injected.version.code=${o.versionCode}`, `-PversionCode=${o.versionCode}`] : []),
|
||||
...(o.versionName ? [`-Pandroid.injected.version.name=${o.versionName}`, `-PversionName=${o.versionName}`] : [])];
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue