catalyst-core 1.1.0 → 1.2.0-canary.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/catalyst.js +54 -6
- package/changelog.md +27 -1
- package/dist/errors/registry.js +306 -18
- package/dist/errors-index.json +273 -70
- package/dist/native/androidProject/app/build.gradle.kts +53 -79
- package/dist/native/androidProject/app/src/main/AndroidManifest.xml +1 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CatalystConstants.kt +3 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +32 -4
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/KeyboardUtil.kt +5 -1
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/ConfigMappingTest.kt +25 -0
- package/dist/native/androidSetup.js +76 -170
- package/dist/native/androidToolchain.js +642 -0
- package/dist/native/appIcon.js +100 -0
- package/dist/native/buildAndroid/assets.js +29 -5
- package/dist/native/buildAndroid/build.js +110 -223
- package/dist/native/buildAndroid/config.js +57 -65
- package/dist/native/buildAndroid/devices.js +176 -0
- package/dist/native/buildAndroid/index.js +271 -46
- package/dist/native/buildAndroid/output.js +43 -0
- package/dist/native/buildAndroid/signing.js +456 -0
- package/dist/native/buildAppAndroid.js +27 -7
- package/dist/native/buildAppIos.js +139 -20
- package/dist/native/buildIos/assets.js +76 -4
- package/dist/native/buildIos/build.js +31 -181
- package/dist/native/buildIos/config.js +126 -11
- package/dist/native/buildIos/index.js +354 -34
- package/dist/native/buildIos/localhostCert.js +87 -0
- package/dist/native/buildIos/output.js +28 -0
- package/dist/native/buildIos/signing.js +596 -0
- package/dist/native/credentials.js +309 -0
- package/dist/native/exportCredentials.js +51 -0
- package/dist/native/iosToolchain.js +583 -0
- package/dist/native/iosnativeWebView/Configurations/Shared.xcconfig +1 -3
- package/dist/native/iosnativeWebView/iosnativeWebView/Info-Release.plist +2 -0
- package/dist/native/iosnativeWebView/iosnativeWebView/PrivacyInfo.xcprivacy +40 -0
- package/dist/native/iosnativeWebView/iosnativeWebView/iosnativeWebView.entitlements +0 -2
- package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.pbxproj +4 -4
- package/dist/native/localState.js +148 -0
- package/dist/native/nativeConfig.js +621 -0
- package/dist/native/releaseOutput.js +53 -0
- package/dist/native/renameAndroidProject.js +154 -369
- package/dist/native/setupEmulatorIos.js +58 -275
- package/dist/native/setupServer.js +520 -124
- package/dist/native/utils.js +418 -115
- package/dist/vite/resolveDevServerConfig.js +21 -0
- package/mcp_v2/knowledge-base.json +13 -11
- package/mcp_v2/test/config.test.ts +40 -0
- package/mcp_v2/tools/build.js +80 -104
- package/mcp_v2/tools/config.js +181 -174
- package/package.json +1 -1
- package/test/cli/nativeFlags.test.ts +97 -0
- package/test/native/androidDevices.test.ts +230 -0
- package/test/native/androidElfAlignment.test.ts +148 -0
- package/test/native/androidSigning.test.ts +646 -0
- package/test/native/androidToolchain.test.ts +766 -0
- package/test/native/appIcon.test.ts +124 -0
- package/test/native/credentials.test.ts +241 -0
- package/test/native/interaction.test.ts +179 -0
- package/test/native/iosConfigPhase.test.ts +611 -0
- package/test/native/iosSigning.test.ts +1060 -0
- package/test/native/iosToolchain.test.ts +821 -0
- package/test/native/localState.test.ts +80 -0
- package/test/native/nativeConfig.test.ts +602 -0
- package/test/native/setupServer.test.ts +529 -0
- package/test/native/utils.test.ts +102 -127
- package/test/vite/resolveDevServerConfig.test.js +25 -0
- package/dist/native/androidProject/keystore/Untitled +0 -0
- package/dist/native/androidProject/keystore/release-key.jks +0 -0
- package/dist/native/iosnativeWebView/iosnativeWebView/localhost.p12 +0 -0
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.PLAY_UPLOAD_KEY_RESET_URL = exports.CREDENTIALS_DIR = void 0;
|
|
7
|
+
exports.configHasPasswords = configHasPasswords;
|
|
8
|
+
exports.createUploadKeystore = createUploadKeystore;
|
|
9
|
+
exports.distinguishedName = distinguishedName;
|
|
10
|
+
exports.ensureGitignored = ensureGitignored;
|
|
11
|
+
exports.exportUploadCertificate = exportUploadCertificate;
|
|
12
|
+
exports.keyPaths = keyPaths;
|
|
13
|
+
exports.legacyKeystorePath = legacyKeystorePath;
|
|
14
|
+
exports.readKeystoreProperties = readKeystoreProperties;
|
|
15
|
+
exports.resolveAndroidSigning = resolveAndroidSigning;
|
|
16
|
+
exports.signingGradleEnv = signingGradleEnv;
|
|
17
|
+
exports.verifyKeystore = verifyKeystore;
|
|
18
|
+
exports.writeKeystoreProperties = writeKeystoreProperties;
|
|
19
|
+
var _child_process = require("child_process");
|
|
20
|
+
var _crypto = _interopRequireDefault(require("crypto"));
|
|
21
|
+
var _fs = _interopRequireDefault(require("fs"));
|
|
22
|
+
var _path = _interopRequireDefault(require("path"));
|
|
23
|
+
var _utils = require("../utils.js");
|
|
24
|
+
var _credentials = require("../credentials.js");
|
|
25
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
26
|
+
// Release signing for Android: one persistent upload key per app (#508).
|
|
27
|
+
//
|
|
28
|
+
// The key used to be generated inside the temp_build_<pkg> copy, which is
|
|
29
|
+
// wiped on every run, so every release was signed with a new key and Play
|
|
30
|
+
// rejected all but the first upload. It now lives in the app, outside git:
|
|
31
|
+
//
|
|
32
|
+
// <app>/credentials/android/upload.jks
|
|
33
|
+
// <app>/credentials/android/keystore.properties (0600: alias + passwords)
|
|
34
|
+
//
|
|
35
|
+
// CI gets the same folder from the CATALYST_ANDROID_CREDENTIALS secret
|
|
36
|
+
// (catalyst credentials:export android), through resolveCredentials.
|
|
37
|
+
// Passwords never go on a command line: keytool reads them with
|
|
38
|
+
// -storepass:env, Gradle through ORG_GRADLE_PROJECT_* variables. No code
|
|
39
|
+
// path here deletes or overwrites an existing key.
|
|
40
|
+
|
|
41
|
+
const CREDENTIALS_DIR = exports.CREDENTIALS_DIR = _path.default.join("credentials", "android");
|
|
42
|
+
const DEFAULT_ALIAS = "upload";
|
|
43
|
+
const DEFAULT_STORE_FILE = "upload.jks";
|
|
44
|
+
const GITIGNORE_ENTRY = "credentials/";
|
|
45
|
+
// Play Console Help, "Use Play App Signing": resetting a lost or compromised upload key.
|
|
46
|
+
const PLAY_UPLOAD_KEY_RESET_URL = exports.PLAY_UPLOAD_KEY_RESET_URL = "https://support.google.com/googleplay/android-developer/answer/9842756";
|
|
47
|
+
const BACKUP_REMINDER = "Back it up now: run catalyst credentials:export android and keep the value somewhere safe, such as a password manager. Play only accepts updates signed with this key.";
|
|
48
|
+
|
|
49
|
+
/** The key files in a credentials folder; keystore.properties may name another storeFile in the same folder. */
|
|
50
|
+
function keyPaths(dir) {
|
|
51
|
+
const propertiesFile = _path.default.join(dir, "keystore.properties");
|
|
52
|
+
const named = _path.default.basename(String(readKeystoreProperties(propertiesFile).storeFile || DEFAULT_STORE_FILE));
|
|
53
|
+
return {
|
|
54
|
+
dir,
|
|
55
|
+
storeFile: _path.default.join(dir, named),
|
|
56
|
+
propertiesFile
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Where releases before 1.1.1 left a copy of their keystore:
|
|
62
|
+
* <android.outputPath or build-output>/<name>-keystore.jks, with the name
|
|
63
|
+
* those releases used (newProjectName, then appName, then the last part of
|
|
64
|
+
* packageName). Pass WEBVIEW_CONFIG.android as written in config.json, not
|
|
65
|
+
* the resolved block: appId and appName overwrite packageName and appName.
|
|
66
|
+
*
|
|
67
|
+
* @param {Record<string, any>} [androidConfig]
|
|
68
|
+
* @param {object} [options]
|
|
69
|
+
* @param {string} [options.cwd]
|
|
70
|
+
*/
|
|
71
|
+
function legacyKeystorePath(androidConfig = {}, {
|
|
72
|
+
cwd = process.cwd()
|
|
73
|
+
} = {}) {
|
|
74
|
+
const name = androidConfig.newProjectName || androidConfig.appName || androidConfig.packageName?.split(".").pop() || "catalystapp";
|
|
75
|
+
const outputPath = androidConfig.outputPath || _path.default.join(cwd, "build-output");
|
|
76
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - outputPath and the name come from the app's own config.
|
|
77
|
+
return _path.default.resolve(cwd, outputPath, `${name}-keystore.jks`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Java .properties, the subset Catalyst writes: key=value lines, # comments. */
|
|
81
|
+
function readKeystoreProperties(file) {
|
|
82
|
+
const values = {};
|
|
83
|
+
if (!_fs.default.existsSync(file)) return values;
|
|
84
|
+
for (const line of _fs.default.readFileSync(file, "utf8").split(/\r?\n/)) {
|
|
85
|
+
const trimmed = line.trim();
|
|
86
|
+
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("!")) continue;
|
|
87
|
+
const index = trimmed.search(/[=:]/);
|
|
88
|
+
if (index === -1) continue;
|
|
89
|
+
values[trimmed.slice(0, index).trim()] = trimmed.slice(index + 1).trim();
|
|
90
|
+
}
|
|
91
|
+
return values;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Write keystore.properties for a new key. Refuses to replace an existing file. */
|
|
95
|
+
function writeKeystoreProperties(file, {
|
|
96
|
+
keyAlias,
|
|
97
|
+
storePassword,
|
|
98
|
+
keyPassword
|
|
99
|
+
}) {
|
|
100
|
+
_fs.default.mkdirSync(_path.default.dirname(file), {
|
|
101
|
+
recursive: true,
|
|
102
|
+
mode: 0o700
|
|
103
|
+
});
|
|
104
|
+
const body = ["# Catalyst Android upload key. Keep this file and upload.jks out of git and backed up:", "# Play only accepts updates signed with this key.", "storeFile=upload.jks", `keyAlias=${keyAlias}`, `storePassword=${storePassword}`, `keyPassword=${keyPassword}`, ""].join("\n");
|
|
105
|
+
// "wx": fail rather than replace the passwords of a key that exists.
|
|
106
|
+
_fs.default.writeFileSync(file, body, {
|
|
107
|
+
mode: 0o600,
|
|
108
|
+
flag: "wx"
|
|
109
|
+
});
|
|
110
|
+
_fs.default.chmodSync(file, 0o600);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Append `credentials/` to the app's .gitignore unless something already covers it. */
|
|
114
|
+
function ensureGitignored(appRoot) {
|
|
115
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - appRoot is the local app directory; the file name is fixed.
|
|
116
|
+
const file = _path.default.join(appRoot, ".gitignore");
|
|
117
|
+
// One descriptor for the read and the append ("a+" creates the file when
|
|
118
|
+
// it is missing), so nothing can swap the file between the two.
|
|
119
|
+
const fd = _fs.default.openSync(file, "a+");
|
|
120
|
+
try {
|
|
121
|
+
const content = _fs.default.readFileSync(fd, "utf8");
|
|
122
|
+
const covered = content.split(/\r?\n/).map(line => line.trim()).some(line => ["credentials", "credentials/", "/credentials", "/credentials/"].includes(line));
|
|
123
|
+
if (covered) return false;
|
|
124
|
+
const prefix = content && !content.endsWith("\n") ? "\n" : "";
|
|
125
|
+
_fs.default.writeSync(fd, `${prefix}${GITIGNORE_ENTRY}\n`);
|
|
126
|
+
return true;
|
|
127
|
+
} finally {
|
|
128
|
+
_fs.default.closeSync(fd);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function generatePassword() {
|
|
132
|
+
return _crypto.default.randomBytes(24).toString("base64url");
|
|
133
|
+
}
|
|
134
|
+
function keytoolPath(env = process.env) {
|
|
135
|
+
if (env.JAVA_HOME) {
|
|
136
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - JAVA_HOME is the local JDK location.
|
|
137
|
+
const candidate = _path.default.join(env.JAVA_HOME, "bin", process.platform === "win32" ? "keytool.exe" : "keytool");
|
|
138
|
+
if (_fs.default.existsSync(candidate)) return candidate;
|
|
139
|
+
}
|
|
140
|
+
return "keytool";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* keytool with the passwords handed over in its environment, never argv.
|
|
145
|
+
*
|
|
146
|
+
* @param {string[]} args
|
|
147
|
+
* @param {{ storePassword: string, keyPassword?: string }} passwords
|
|
148
|
+
* @param {{ env?: Record<string, string | undefined>, exec?: Function }} [options]
|
|
149
|
+
*/
|
|
150
|
+
function runKeytool(args, passwords, {
|
|
151
|
+
env = process.env,
|
|
152
|
+
exec = _child_process.execFileSync
|
|
153
|
+
} = {}) {
|
|
154
|
+
const childEnv = {
|
|
155
|
+
...env,
|
|
156
|
+
CATALYST_KEYTOOL_STOREPASS: passwords.storePassword
|
|
157
|
+
};
|
|
158
|
+
if (passwords.keyPassword !== undefined) childEnv.CATALYST_KEYTOOL_KEYPASS = passwords.keyPassword;
|
|
159
|
+
return exec(keytoolPath(env), args, {
|
|
160
|
+
env: childEnv,
|
|
161
|
+
encoding: "utf8",
|
|
162
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
function distinguishedName(organizationInfo = {}, commonName) {
|
|
166
|
+
const escape = value => String(value).replace(/([,\\+"<>;=])/g, "\\$1");
|
|
167
|
+
const org = organizationInfo.organization || organizationInfo.companyName;
|
|
168
|
+
const parts = [["CN", organizationInfo.companyName || commonName || "Catalyst App"], ["OU", organizationInfo.department], ["O", org], ["L", organizationInfo.city], ["ST", organizationInfo.state], ["C", organizationInfo.countryCode]];
|
|
169
|
+
return parts.filter(([, value]) => value).map(([key, value]) => `${key}=${escape(value)}`).join(", ");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Create a PKCS12 upload keystore. PKCS12 uses one password for the store
|
|
174
|
+
* and the key, so `password` is both. Never replaces an existing file.
|
|
175
|
+
*
|
|
176
|
+
* @param {string} storeFile
|
|
177
|
+
* @param {object} key
|
|
178
|
+
* @param {string} [key.keyAlias]
|
|
179
|
+
* @param {string} key.password
|
|
180
|
+
* @param {Record<string, string>} [key.organizationInfo] - from android.keystoreConfig
|
|
181
|
+
* @param {string} [key.commonName] - CN when organizationInfo has no companyName
|
|
182
|
+
* @param {number} [key.validityYears]
|
|
183
|
+
* @param {{ env?: Record<string, string | undefined>, exec?: Function }} [options]
|
|
184
|
+
*/
|
|
185
|
+
function createUploadKeystore(storeFile, {
|
|
186
|
+
keyAlias = DEFAULT_ALIAS,
|
|
187
|
+
password,
|
|
188
|
+
organizationInfo,
|
|
189
|
+
commonName,
|
|
190
|
+
validityYears = 25
|
|
191
|
+
}, options = {}) {
|
|
192
|
+
if (_fs.default.existsSync(storeFile)) {
|
|
193
|
+
throw new Error(`Refusing to replace the existing upload key at ${storeFile}`);
|
|
194
|
+
}
|
|
195
|
+
_fs.default.mkdirSync(_path.default.dirname(storeFile), {
|
|
196
|
+
recursive: true,
|
|
197
|
+
mode: 0o700
|
|
198
|
+
});
|
|
199
|
+
const args = ["-genkeypair", "-storetype", "PKCS12", "-keystore", storeFile, "-alias", keyAlias, "-keyalg", "RSA", "-keysize", "2048", "-validity", String(validityYears * 365), "-storepass:env", "CATALYST_KEYTOOL_STOREPASS", "-keypass:env", "CATALYST_KEYTOOL_KEYPASS", "-dname", distinguishedName(organizationInfo, commonName)];
|
|
200
|
+
try {
|
|
201
|
+
runKeytool(args, {
|
|
202
|
+
storePassword: password,
|
|
203
|
+
keyPassword: password
|
|
204
|
+
}, options);
|
|
205
|
+
} catch (error) {
|
|
206
|
+
const detail = String(error.stderr || error.message || "").trim().split("\n")[0];
|
|
207
|
+
throw new Error(`keytool could not create the upload key: ${detail}`);
|
|
208
|
+
}
|
|
209
|
+
_fs.default.chmodSync(storeFile, 0o600);
|
|
210
|
+
return storeFile;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Fail before Gradle when the store, password or alias don't match.
|
|
215
|
+
*
|
|
216
|
+
* @param {{ storeFile: string, keyAlias: string, storePassword: string }} signing
|
|
217
|
+
* @param {{ env?: Record<string, string | undefined>, exec?: Function }} [options]
|
|
218
|
+
*/
|
|
219
|
+
function verifyKeystore(signing, options = {}) {
|
|
220
|
+
try {
|
|
221
|
+
runKeytool(["-list", "-keystore", signing.storeFile, "-alias", signing.keyAlias, "-storepass:env", "CATALYST_KEYTOOL_STOREPASS"], {
|
|
222
|
+
storePassword: signing.storePassword
|
|
223
|
+
}, options);
|
|
224
|
+
} catch (error) {
|
|
225
|
+
if (error.code === "ENOENT") throw error;
|
|
226
|
+
const detail = String(error.stderr || error.stdout || error.message || "").trim().split("\n").pop();
|
|
227
|
+
throw (0, _utils.nativeError)("ANDROID-006", `keytool could not open ${signing.storeFile} with alias "${signing.keyAlias}"${detail ? ` (${detail})` : ""}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** True when config.json carries keystore passwords, which belong in credentials/ or CI secrets. */
|
|
232
|
+
function configHasPasswords(androidConfig = {}) {
|
|
233
|
+
const blocks = [androidConfig.keystoreConfig, androidConfig.keystore].filter(Boolean);
|
|
234
|
+
return blocks.some(block => block.storePassword || block.keyPassword);
|
|
235
|
+
}
|
|
236
|
+
function legacyPasswords(androidConfig = {}) {
|
|
237
|
+
if (androidConfig.keystoreConfig?.storePassword) {
|
|
238
|
+
return {
|
|
239
|
+
keyAlias: androidConfig.keystoreConfig.keyAlias || "release",
|
|
240
|
+
password: androidConfig.keystoreConfig.storePassword
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
if (androidConfig.keystore?.storePassword) {
|
|
244
|
+
return {
|
|
245
|
+
keyAlias: androidConfig.keystore.alias || "release",
|
|
246
|
+
password: androidConfig.keystore.storePassword
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Write the upload certificate as PEM (`keytool -exportcert -rfc`). Play asks
|
|
254
|
+
* for it when an upload key is registered or reset, and it identifies which
|
|
255
|
+
* key signed a bundle (#508).
|
|
256
|
+
*
|
|
257
|
+
* @param {{ storeFile: string, keyAlias: string, storePassword: string }} signing
|
|
258
|
+
* @param {string} destFile
|
|
259
|
+
* @param {{ env?: Record<string, string | undefined>, exec?: Function }} [options]
|
|
260
|
+
* @returns {string} destFile
|
|
261
|
+
*/
|
|
262
|
+
function exportUploadCertificate(signing, destFile, options = {}) {
|
|
263
|
+
_fs.default.mkdirSync(_path.default.dirname(destFile), {
|
|
264
|
+
recursive: true
|
|
265
|
+
});
|
|
266
|
+
try {
|
|
267
|
+
runKeytool(["-exportcert", "-rfc", "-keystore", signing.storeFile, "-alias", signing.keyAlias, "-storepass:env", "CATALYST_KEYTOOL_STOREPASS", "-file", destFile], {
|
|
268
|
+
storePassword: signing.storePassword
|
|
269
|
+
}, options);
|
|
270
|
+
} catch (error) {
|
|
271
|
+
const detail = String(error.stderr || error.message || "").trim().split("\n")[0];
|
|
272
|
+
throw new Error(`keytool could not export the upload certificate: ${detail}`);
|
|
273
|
+
}
|
|
274
|
+
return destFile;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Work out which upload key signs this release.
|
|
279
|
+
*
|
|
280
|
+
* The key comes from credentials/android/ (upload.jks and keystore.properties),
|
|
281
|
+
* or in CI from the CATALYST_ANDROID_CREDENTIALS secret (resolveCredentials).
|
|
282
|
+
* When there is none, a person at a terminal may adopt a keystore an older
|
|
283
|
+
* release left in build-output/, or create a new key. Without a terminal no
|
|
284
|
+
* key is ever created, whatever the flags. A new key is refused for an app
|
|
285
|
+
* that is already on Google Play: its updates must be signed with the key
|
|
286
|
+
* Play knows, and only Play can reset that.
|
|
287
|
+
*
|
|
288
|
+
* Every password read is masked in GitHub Actions logs (maskSecret).
|
|
289
|
+
*
|
|
290
|
+
* @param {object} [options]
|
|
291
|
+
* @param {string} [options.appRoot]
|
|
292
|
+
* @param {Record<string, any>} [options.androidConfig] - WEBVIEW_CONFIG.android
|
|
293
|
+
* @param {Record<string, string | undefined>} [options.env]
|
|
294
|
+
* @param {boolean} [options.isTTY]
|
|
295
|
+
* @param {(question: string, options?: object) => Promise<string>} [options.prompt]
|
|
296
|
+
* @param {(message: string, type?: string) => void} [options.log]
|
|
297
|
+
* @param {string} [options.legacyKeystorePath] - build-output/<name>-keystore.jks from earlier releases
|
|
298
|
+
* @param {string} [options.commonName] - certificate CN for a new key
|
|
299
|
+
* @param {Function} [options.exec] - execFileSync stand-in, for tests
|
|
300
|
+
* @param {(value: string, options?: object) => unknown} [options.mask] - maskSecret stand-in, for tests
|
|
301
|
+
* @returns {Promise<{storeFile: string, storePassword: string, keyAlias: string, keyPassword: string, source: string, created: boolean, cleanup: () => void}>}
|
|
302
|
+
*/
|
|
303
|
+
async function resolveAndroidSigning({
|
|
304
|
+
appRoot = process.cwd(),
|
|
305
|
+
androidConfig = {},
|
|
306
|
+
env = process.env,
|
|
307
|
+
isTTY = Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
308
|
+
prompt = _utils.promptUser,
|
|
309
|
+
log = () => {},
|
|
310
|
+
legacyKeystorePath,
|
|
311
|
+
commonName,
|
|
312
|
+
exec,
|
|
313
|
+
mask = _credentials.maskSecret
|
|
314
|
+
} = {}) {
|
|
315
|
+
const keytoolOptions = {
|
|
316
|
+
env,
|
|
317
|
+
...(exec ? {
|
|
318
|
+
exec
|
|
319
|
+
} : {})
|
|
320
|
+
};
|
|
321
|
+
if (configHasPasswords(androidConfig)) {
|
|
322
|
+
log("config/config.json holds keystore passwords (android.keystoreConfig or android.keystore). " + "Move them to credentials/android/keystore.properties and delete them from the config.", "warning");
|
|
323
|
+
}
|
|
324
|
+
const credentials = (0, _credentials.resolveCredentials)("android", {
|
|
325
|
+
cwd: appRoot,
|
|
326
|
+
env,
|
|
327
|
+
log
|
|
328
|
+
});
|
|
329
|
+
const {
|
|
330
|
+
cleanup
|
|
331
|
+
} = credentials;
|
|
332
|
+
const paths = keyPaths(credentials.dir);
|
|
333
|
+
const where = credentials.source === "env" ? credentials.envName : _path.default.relative(appRoot, paths.dir) || CREDENTIALS_DIR;
|
|
334
|
+
const fail = (code, detail, extra) => {
|
|
335
|
+
cleanup();
|
|
336
|
+
return (0, _utils.nativeError)(code, detail, extra);
|
|
337
|
+
};
|
|
338
|
+
const finish = (storeFile, source, created = false) => {
|
|
339
|
+
const values = readKeystoreProperties(paths.propertiesFile);
|
|
340
|
+
const storePassword = values.storePassword;
|
|
341
|
+
const keyAlias = values.keyAlias || DEFAULT_ALIAS;
|
|
342
|
+
const keyPassword = values.keyPassword || storePassword;
|
|
343
|
+
if (!storePassword) {
|
|
344
|
+
throw fail("ANDROID-006", `no storePassword in keystore.properties (${where})`);
|
|
345
|
+
}
|
|
346
|
+
mask(storePassword, {
|
|
347
|
+
env
|
|
348
|
+
});
|
|
349
|
+
if (keyPassword !== storePassword) mask(keyPassword, {
|
|
350
|
+
env
|
|
351
|
+
});
|
|
352
|
+
const signing = {
|
|
353
|
+
storeFile,
|
|
354
|
+
storePassword,
|
|
355
|
+
keyAlias,
|
|
356
|
+
keyPassword,
|
|
357
|
+
source,
|
|
358
|
+
created,
|
|
359
|
+
cleanup
|
|
360
|
+
};
|
|
361
|
+
try {
|
|
362
|
+
verifyKeystore(signing, keytoolOptions);
|
|
363
|
+
} catch (error) {
|
|
364
|
+
cleanup();
|
|
365
|
+
throw error;
|
|
366
|
+
}
|
|
367
|
+
return signing;
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
// 1. The key is there: the app's folder, or the CI secret.
|
|
371
|
+
if (_fs.default.existsSync(paths.storeFile)) {
|
|
372
|
+
if (credentials.source === "folder") ensureGitignored(appRoot);
|
|
373
|
+
return finish(paths.storeFile, credentials.source === "env" ? credentials.envName : CREDENTIALS_DIR);
|
|
374
|
+
}
|
|
375
|
+
if (credentials.source === "env") {
|
|
376
|
+
throw fail("ANDROID-006", `${credentials.envName} has no ${_path.default.basename(paths.storeFile)}`);
|
|
377
|
+
}
|
|
378
|
+
// Settings without their key: the key was lost or not copied. A new key
|
|
379
|
+
// would not match the one Play knows, so stop here.
|
|
380
|
+
if (_fs.default.existsSync(paths.propertiesFile)) {
|
|
381
|
+
throw fail("ANDROID-006", `${where}/keystore.properties is there but ${_path.default.basename(paths.storeFile)} is not; restore the key from your backup`);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// 2. Nothing yet. Adopting or creating a key needs a person to say yes.
|
|
385
|
+
const legacy = legacyKeystorePath && _fs.default.existsSync(legacyKeystorePath) ? legacyKeystorePath : null;
|
|
386
|
+
const legacyPw = legacyPasswords(androidConfig);
|
|
387
|
+
const relStore = _path.default.relative(appRoot, paths.storeFile);
|
|
388
|
+
if (!isTTY) {
|
|
389
|
+
const detail = legacy ? `found ${legacy} from an earlier build; run the release build once in a terminal to adopt it` : `nothing in ${CREDENTIALS_DIR} and no ${credentials.envName}`;
|
|
390
|
+
throw fail("ANDROID-005", detail);
|
|
391
|
+
}
|
|
392
|
+
if (legacy && legacyPw) {
|
|
393
|
+
const adopt = await (0, _utils.askConsent)("key", `Found ${_path.default.relative(appRoot, legacy)} from an earlier release build. Keep using it as the upload key (copies it to ${relStore})`, {
|
|
394
|
+
isTTY,
|
|
395
|
+
prompt
|
|
396
|
+
});
|
|
397
|
+
if (adopt) {
|
|
398
|
+
_fs.default.mkdirSync(paths.dir, {
|
|
399
|
+
recursive: true,
|
|
400
|
+
mode: 0o700
|
|
401
|
+
});
|
|
402
|
+
_fs.default.copyFileSync(legacy, paths.storeFile, _fs.default.constants.COPYFILE_EXCL);
|
|
403
|
+
_fs.default.chmodSync(paths.storeFile, 0o600);
|
|
404
|
+
writeKeystoreProperties(paths.propertiesFile, {
|
|
405
|
+
keyAlias: legacyPw.keyAlias,
|
|
406
|
+
storePassword: legacyPw.password,
|
|
407
|
+
keyPassword: legacyPw.password
|
|
408
|
+
});
|
|
409
|
+
if (ensureGitignored(appRoot)) log("Added credentials/ to .gitignore", "info");
|
|
410
|
+
log(`Upload key adopted at ${relStore}. ${BACKUP_REMINDER}`, "warning");
|
|
411
|
+
return finish(paths.storeFile, "adopted");
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
const create = await (0, _utils.askConsent)("key", `No upload key found. Create one in ${CREDENTIALS_DIR}/ (PKCS12, kept out of git)`, {
|
|
415
|
+
isTTY,
|
|
416
|
+
prompt
|
|
417
|
+
});
|
|
418
|
+
if (!create) throw fail("ANDROID-005", "creating a new upload key was declined");
|
|
419
|
+
const onPlay = await (0, _utils.confirm)("Has this app been uploaded to Google Play before", {
|
|
420
|
+
defaultYes: false,
|
|
421
|
+
prompt,
|
|
422
|
+
isTTY: true
|
|
423
|
+
});
|
|
424
|
+
if (onPlay) {
|
|
425
|
+
throw fail("ANDROID-005", "this app is already on Google Play, so a new upload key would be rejected", {
|
|
426
|
+
hint: `Put the upload key the app was signed with in ${CREDENTIALS_DIR}/ (upload.jks and keystore.properties). If it is lost, ask Google Play to reset the upload key: ${PLAY_UPLOAD_KEY_RESET_URL}`
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
const keyAlias = DEFAULT_ALIAS;
|
|
430
|
+
const password = generatePassword();
|
|
431
|
+
createUploadKeystore(paths.storeFile, {
|
|
432
|
+
keyAlias,
|
|
433
|
+
password,
|
|
434
|
+
organizationInfo: androidConfig.keystoreConfig?.organizationInfo,
|
|
435
|
+
commonName,
|
|
436
|
+
validityYears: androidConfig.keystoreConfig?.validityYears || 25
|
|
437
|
+
}, keytoolOptions);
|
|
438
|
+
writeKeystoreProperties(paths.propertiesFile, {
|
|
439
|
+
keyAlias,
|
|
440
|
+
storePassword: password,
|
|
441
|
+
keyPassword: password
|
|
442
|
+
});
|
|
443
|
+
if (ensureGitignored(appRoot)) log("Added credentials/ to .gitignore", "info");
|
|
444
|
+
log(`Created the upload key at ${relStore}. ${BACKUP_REMINDER}`, "warning");
|
|
445
|
+
return finish(paths.storeFile, "created", true);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/** The Gradle project properties that sign the release, as environment variables. */
|
|
449
|
+
function signingGradleEnv(signing) {
|
|
450
|
+
return {
|
|
451
|
+
ORG_GRADLE_PROJECT_catalystStoreFile: signing.storeFile,
|
|
452
|
+
ORG_GRADLE_PROJECT_catalystStorePassword: signing.storePassword,
|
|
453
|
+
ORG_GRADLE_PROJECT_catalystKeyAlias: signing.keyAlias,
|
|
454
|
+
ORG_GRADLE_PROJECT_catalystKeyPassword: signing.keyPassword
|
|
455
|
+
};
|
|
456
|
+
}
|
|
@@ -11,18 +11,35 @@ const {
|
|
|
11
11
|
const {
|
|
12
12
|
loadAppConfig
|
|
13
13
|
} = require("../cli/appConfig.js");
|
|
14
|
+
const {
|
|
15
|
+
buildFailure
|
|
16
|
+
} = require("../cli/diagnostic.js");
|
|
17
|
+
const {
|
|
18
|
+
parseNativeArgs
|
|
19
|
+
} = require("./utils.js");
|
|
14
20
|
const {
|
|
15
21
|
WEBVIEW_CONFIG,
|
|
16
22
|
BUILD_OUTPUT_PATH
|
|
17
23
|
} = loadAppConfig();
|
|
18
24
|
async function main() {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
})
|
|
25
|
+
let flags;
|
|
26
|
+
try {
|
|
27
|
+
flags = parseNativeArgs(process.argv.slice(2));
|
|
28
|
+
} catch (error) {
|
|
29
|
+
// `--url` without a value (NATIVE-004), before any build step ran.
|
|
30
|
+
process.stderr.write(buildFailure({
|
|
31
|
+
error,
|
|
32
|
+
stage: "Reading the flags",
|
|
33
|
+
code: "ANDROID-000"
|
|
34
|
+
}));
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
23
37
|
const {
|
|
24
38
|
buildAndroidApp
|
|
25
|
-
} =
|
|
39
|
+
} = createAndroidBuild({
|
|
40
|
+
WEBVIEW_CONFIG,
|
|
41
|
+
BUILD_OUTPUT_PATH
|
|
42
|
+
}, flags);
|
|
26
43
|
try {
|
|
27
44
|
await buildAndroidApp();
|
|
28
45
|
} catch (error) {
|
|
@@ -30,9 +47,12 @@ async function main() {
|
|
|
30
47
|
// the troubleshooting guide, so the default mode does not repeat it.
|
|
31
48
|
// --verbose/--debug add the registry's boxed report on top.
|
|
32
49
|
if (resolveOutputMode() !== "default") {
|
|
50
|
+
// A coded error (ANDROID-005 and on, NATIVE-*) is our own; anything else is Gradle's.
|
|
51
|
+
const code = /^(ANDROID|NATIVE)-\d{3}$/.test(error?.code) ? error.code : "ANDROID-000";
|
|
52
|
+
const category = code.split("-")[0];
|
|
33
53
|
console.error(formatBuildError({
|
|
34
|
-
code
|
|
35
|
-
category
|
|
54
|
+
code,
|
|
55
|
+
category,
|
|
36
56
|
upstreamName: "Gradle",
|
|
37
57
|
error
|
|
38
58
|
}));
|