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,596 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.SIGNING_PROPERTIES = exports.EXPORT_METHODS = void 0;
|
|
7
|
+
exports.buildExportOptions = buildExportOptions;
|
|
8
|
+
exports.isReleaseBuildType = isReleaseBuildType;
|
|
9
|
+
exports.isSigningFailure = isSigningFailure;
|
|
10
|
+
exports.parseIdentity = parseIdentity;
|
|
11
|
+
exports.parseKeychainList = parseKeychainList;
|
|
12
|
+
exports.parseProperties = parseProperties;
|
|
13
|
+
exports.planIosSigning = planIosSigning;
|
|
14
|
+
exports.profileDirectories = profileDirectories;
|
|
15
|
+
exports.profileMatchesBundleId = profileMatchesBundleId;
|
|
16
|
+
exports.readIosCredentials = readIosCredentials;
|
|
17
|
+
exports.readProvisioningProfile = readProvisioningProfile;
|
|
18
|
+
exports.resolveExportMethod = resolveExportMethod;
|
|
19
|
+
exports.setupP12Signing = setupP12Signing;
|
|
20
|
+
exports.signingSetupError = signingSetupError;
|
|
21
|
+
exports.writeExportOptionsPlist = writeExportOptionsPlist;
|
|
22
|
+
exports.xcodeAuthArgs = xcodeAuthArgs;
|
|
23
|
+
var _child_process = require("child_process");
|
|
24
|
+
var _crypto = _interopRequireDefault(require("crypto"));
|
|
25
|
+
var _fs = _interopRequireDefault(require("fs"));
|
|
26
|
+
var _os = _interopRequireDefault(require("os"));
|
|
27
|
+
var _path = _interopRequireDefault(require("path"));
|
|
28
|
+
var _utils = require("../utils.js");
|
|
29
|
+
var _credentials = require("../credentials.js");
|
|
30
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
31
|
+
// iOS signing without opening Xcode. Signing files live in the app's
|
|
32
|
+
// git-ignored credentials/ios/ folder, or in CI in the one
|
|
33
|
+
// CATALYST_IOS_CREDENTIALS secret (`catalyst credentials:export ios`), which
|
|
34
|
+
// resolveCredentials("ios") unpacks to a private temp folder:
|
|
35
|
+
//
|
|
36
|
+
// credentials/ios/
|
|
37
|
+
// AuthKey_<KEYID>.p8 App Store Connect API key: cloud signing
|
|
38
|
+
// <name>.p12 distribution certificate and its private key: local signing
|
|
39
|
+
// <name>.mobileprovision the provisioning profile for that certificate
|
|
40
|
+
// signing.properties ascIssuerId=<uuid>, ascKeyId=<id> (optional, else from
|
|
41
|
+
// the .p8 file name), p12Password=<password of the .p12>
|
|
42
|
+
//
|
|
43
|
+
// A release signs, in this order:
|
|
44
|
+
//
|
|
45
|
+
// 1. Locally with the .p12 and profile: the p12 goes into a temporary
|
|
46
|
+
// keychain and the profile into Xcode's profile folders, and both are
|
|
47
|
+
// removed again when the build ends, also on Ctrl+C.
|
|
48
|
+
// 2. Manually with an installed profile: ios.provisioningProfile.
|
|
49
|
+
// 3. Automatically for ios.developmentTeam: cloud signing with the API key,
|
|
50
|
+
// or whatever Apple account is signed in to Xcode.
|
|
51
|
+
//
|
|
52
|
+
// `security` takes the p12 password (`import -P`) and the throwaway keychain
|
|
53
|
+
// password (`create-keychain -p`, `unlock-keychain -p`, `set-key-partition-list
|
|
54
|
+
// -k`) only as arguments: it has no environment or stdin form for them, and
|
|
55
|
+
// leaving them out makes it ask through a GUI prompt. They are never logged,
|
|
56
|
+
// they are masked under GitHub Actions (maskSecret), and every error from
|
|
57
|
+
// these commands is scrubbed of both before it is shown.
|
|
58
|
+
|
|
59
|
+
// Xcode 26's names. The pre-Xcode 15.3 names still work there, with a warning.
|
|
60
|
+
const EXPORT_METHODS = exports.EXPORT_METHODS = ["app-store-connect", "release-testing", "enterprise", "debugging"];
|
|
61
|
+
const LEGACY_EXPORT_METHODS = {
|
|
62
|
+
"app-store": "app-store-connect",
|
|
63
|
+
"ad-hoc": "release-testing",
|
|
64
|
+
development: "debugging"
|
|
65
|
+
};
|
|
66
|
+
const KEYCHAIN_LOCK_TIMEOUT_SECONDS = 6 * 60 * 60;
|
|
67
|
+
const SIGNING_PROPERTIES = exports.SIGNING_PROPERTIES = "signing.properties";
|
|
68
|
+
|
|
69
|
+
/** `ios.buildType: "release"`, in any case, asks for a release build like `--release`. */
|
|
70
|
+
function isReleaseBuildType(buildType) {
|
|
71
|
+
return String(buildType || "").trim().toLowerCase() === "release";
|
|
72
|
+
}
|
|
73
|
+
function defaultRun(command, args, {
|
|
74
|
+
input,
|
|
75
|
+
env
|
|
76
|
+
} = {}) {
|
|
77
|
+
return (0, _child_process.execFileSync)(command, args, {
|
|
78
|
+
encoding: "utf8",
|
|
79
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
80
|
+
input,
|
|
81
|
+
env: env || process.env,
|
|
82
|
+
maxBuffer: 64 * 1024 * 1024
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The export method for `-exportArchive`: ios.exportMethod, default
|
|
88
|
+
* app-store-connect. Legacy names map to their Xcode 26 equivalent.
|
|
89
|
+
*
|
|
90
|
+
* @param {Record<string, any>} [iosConfig]
|
|
91
|
+
* @param {{ log?: (message: string, type?: string) => void }} [options]
|
|
92
|
+
*/
|
|
93
|
+
function resolveExportMethod(iosConfig = {}, {
|
|
94
|
+
log = () => {}
|
|
95
|
+
} = {}) {
|
|
96
|
+
const raw = iosConfig.exportMethod;
|
|
97
|
+
if (raw === undefined || raw === null || String(raw).trim() === "") return "app-store-connect";
|
|
98
|
+
const value = String(raw).trim().toLowerCase();
|
|
99
|
+
if (EXPORT_METHODS.includes(value)) return value;
|
|
100
|
+
if (LEGACY_EXPORT_METHODS[value]) {
|
|
101
|
+
log(`ios.exportMethod "${raw}" is the old name; use "${LEGACY_EXPORT_METHODS[value]}" instead.`, "warning");
|
|
102
|
+
return LEGACY_EXPORT_METHODS[value];
|
|
103
|
+
}
|
|
104
|
+
throw (0, _utils.nativeError)("IOS-013", `"${raw}"`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Java-style .properties, the subset people write by hand: key=value (or key: value) lines, # and ! comments. */
|
|
108
|
+
function parseProperties(text) {
|
|
109
|
+
const values = {};
|
|
110
|
+
for (const raw of String(text || "").split(/\r?\n/)) {
|
|
111
|
+
const line = raw.trim();
|
|
112
|
+
if (!line || line.startsWith("#") || line.startsWith("!")) continue;
|
|
113
|
+
const match = /^([^=:\s]+)\s*[=:]\s*(.*)$/.exec(line);
|
|
114
|
+
if (match) values[match[1]] = match[2].trim();
|
|
115
|
+
}
|
|
116
|
+
return values;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* What credentials/ios/ (or the unpacked CATALYST_IOS_CREDENTIALS) holds.
|
|
121
|
+
* Each password read is masked under GitHub Actions before anything else can
|
|
122
|
+
* print it.
|
|
123
|
+
*
|
|
124
|
+
* @param {string} dir - from resolveCredentials("ios")
|
|
125
|
+
* @param {object} [options]
|
|
126
|
+
* @param {Record<string, string | undefined>} [options.env] - for maskSecret
|
|
127
|
+
* @param {(value: string, options?: object) => boolean} [options.mask]
|
|
128
|
+
* @returns {{
|
|
129
|
+
* dir: string,
|
|
130
|
+
* apiKey: { keyPath: string, keyId: string, issuerId: string } | null,
|
|
131
|
+
* p12: { file: string, password: string, profileFile: string } | null,
|
|
132
|
+
* }}
|
|
133
|
+
*/
|
|
134
|
+
function readIosCredentials(dir, {
|
|
135
|
+
env = process.env,
|
|
136
|
+
mask = _credentials.maskSecret
|
|
137
|
+
} = {}) {
|
|
138
|
+
let names = [];
|
|
139
|
+
try {
|
|
140
|
+
names = _fs.default.readdirSync(dir).filter(name => _fs.default.statSync(_path.default.join(dir, name)).isFile());
|
|
141
|
+
} catch (error) {
|
|
142
|
+
return {
|
|
143
|
+
dir,
|
|
144
|
+
apiKey: null,
|
|
145
|
+
p12: null
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
const withExt = ext => names.filter(name => name.toLowerCase().endsWith(ext));
|
|
149
|
+
const one = (ext, code) => {
|
|
150
|
+
const found = withExt(ext);
|
|
151
|
+
if (found.length > 1) {
|
|
152
|
+
throw (0, _utils.nativeError)(code, `credentials/ios/ holds ${found.length} ${ext} files (${found.join(", ")}); keep one`);
|
|
153
|
+
}
|
|
154
|
+
return found[0] ? _path.default.join(dir, found[0]) : null;
|
|
155
|
+
};
|
|
156
|
+
const props = names.includes(SIGNING_PROPERTIES) ? parseProperties(_fs.default.readFileSync(_path.default.join(dir, SIGNING_PROPERTIES), "utf8")) : {};
|
|
157
|
+
if (props.p12Password) mask(props.p12Password, {
|
|
158
|
+
env
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// App Store Connect API key.
|
|
162
|
+
let apiKey = null;
|
|
163
|
+
const keyFile = one(".p8", "IOS-011");
|
|
164
|
+
if (keyFile || props.ascKeyId || props.ascIssuerId) {
|
|
165
|
+
const fromName = keyFile ? /^AuthKey_([A-Za-z0-9]+)\.p8$/i.exec(_path.default.basename(keyFile)) : null;
|
|
166
|
+
const keyId = String(props.ascKeyId || fromName?.[1] || "").trim();
|
|
167
|
+
const issuerId = String(props.ascIssuerId || "").trim();
|
|
168
|
+
const missing = [];
|
|
169
|
+
if (!keyFile) missing.push("the key file (AuthKey_<KEYID>.p8)");
|
|
170
|
+
if (!keyId) missing.push(`the key ID (ascKeyId in ${SIGNING_PROPERTIES}, or the AuthKey_<KEYID>.p8 file name)`);
|
|
171
|
+
if (!issuerId) missing.push(`the issuer ID (ascIssuerId in ${SIGNING_PROPERTIES})`);
|
|
172
|
+
if (missing.length) throw (0, _utils.nativeError)("IOS-011", `credentials/ios/ is missing ${missing.join(", ")}`);
|
|
173
|
+
if (!/^[A-Za-z0-9]+$/.test(keyId)) {
|
|
174
|
+
throw (0, _utils.nativeError)("IOS-011", `the key ID "${keyId}" should be letters and digits only`);
|
|
175
|
+
}
|
|
176
|
+
apiKey = {
|
|
177
|
+
keyPath: keyFile,
|
|
178
|
+
keyId,
|
|
179
|
+
issuerId
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Distribution certificate and profile.
|
|
184
|
+
let p12 = null;
|
|
185
|
+
const p12File = one(".p12", "IOS-012");
|
|
186
|
+
const profileFile = one(".mobileprovision", "IOS-012");
|
|
187
|
+
if (p12File && !profileFile) {
|
|
188
|
+
throw (0, _utils.nativeError)("IOS-012", "credentials/ios/ has a .p12 but no .mobileprovision for it");
|
|
189
|
+
}
|
|
190
|
+
if (profileFile && !p12File) {
|
|
191
|
+
throw (0, _utils.nativeError)("IOS-012", "credentials/ios/ has a .mobileprovision but no .p12 to sign with");
|
|
192
|
+
}
|
|
193
|
+
if (p12File) p12 = {
|
|
194
|
+
file: p12File,
|
|
195
|
+
password: props.p12Password ?? "",
|
|
196
|
+
profileFile
|
|
197
|
+
};
|
|
198
|
+
return {
|
|
199
|
+
dir,
|
|
200
|
+
apiKey,
|
|
201
|
+
p12
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* xcodebuild flags that let automatic signing create and fetch certificates
|
|
207
|
+
* and profiles: with the API key when there is one (cloud signing),
|
|
208
|
+
* otherwise with the Apple account signed in to Xcode.
|
|
209
|
+
*/
|
|
210
|
+
function xcodeAuthArgs(apiKey) {
|
|
211
|
+
const args = ["-allowProvisioningUpdates", "-allowProvisioningDeviceRegistration"];
|
|
212
|
+
if (apiKey) {
|
|
213
|
+
args.push("-authenticationKeyPath", apiKey.keyPath, "-authenticationKeyID", apiKey.keyId, "-authenticationKeyIssuerID", apiKey.issuerId);
|
|
214
|
+
}
|
|
215
|
+
return args;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* How this build signs, decided from the config and the credentials alone
|
|
220
|
+
* (no side effects), so the entitlements can be written before Xcode runs.
|
|
221
|
+
*
|
|
222
|
+
* aps-environment: Xcode's entitlements merger takes aps-environment from the
|
|
223
|
+
* provisioning profile it signs with. An automatic-signing archive is signed
|
|
224
|
+
* for development and export re-signs it with the distribution profile, so
|
|
225
|
+
* the archive keeps "development". A manual release archive is signed with
|
|
226
|
+
* the distribution profile directly, so it must already say "production".
|
|
227
|
+
*
|
|
228
|
+
* @param {object} options
|
|
229
|
+
* @param {Record<string, any>} [options.iosConfig]
|
|
230
|
+
* @param {ReturnType<typeof readIosCredentials> | null} [options.credentials]
|
|
231
|
+
* @param {boolean} [options.release]
|
|
232
|
+
* @param {string} [options.exportMethod]
|
|
233
|
+
*/
|
|
234
|
+
function planIosSigning({
|
|
235
|
+
iosConfig = {},
|
|
236
|
+
credentials = null,
|
|
237
|
+
release = false,
|
|
238
|
+
exportMethod = "app-store-connect"
|
|
239
|
+
} = {}) {
|
|
240
|
+
const teamId = String(iosConfig.developmentTeam || "").trim();
|
|
241
|
+
const distribution = release && exportMethod !== "debugging";
|
|
242
|
+
if (release && credentials?.p12) {
|
|
243
|
+
return {
|
|
244
|
+
style: "manual",
|
|
245
|
+
source: "credentials/ios .p12",
|
|
246
|
+
p12: true,
|
|
247
|
+
teamId,
|
|
248
|
+
apsEnvironment: distribution ? "production" : "development"
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
if (iosConfig.provisioningProfile) {
|
|
252
|
+
return {
|
|
253
|
+
style: "manual",
|
|
254
|
+
source: "ios.provisioningProfile",
|
|
255
|
+
p12: false,
|
|
256
|
+
teamId,
|
|
257
|
+
provisioningProfile: String(iosConfig.provisioningProfile),
|
|
258
|
+
apsEnvironment: distribution ? "production" : "development"
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
style: "automatic",
|
|
263
|
+
source: credentials?.apiKey ? "App Store Connect API key" : "Xcode account",
|
|
264
|
+
p12: false,
|
|
265
|
+
teamId,
|
|
266
|
+
apsEnvironment: "development"
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** The quoted paths `security list-keychains` prints, one per line. */
|
|
271
|
+
function parseKeychainList(output) {
|
|
272
|
+
return [...String(output || "").matchAll(/"([^"]+)"/g)].map(match => match[1]);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** The first valid identity `security find-identity -v -p codesigning` lists. */
|
|
276
|
+
function parseIdentity(output) {
|
|
277
|
+
const match = /^\s*\d+\)\s+([0-9A-Fa-f]{40})\s+"([^"]+)"/m.exec(String(output || ""));
|
|
278
|
+
return match ? {
|
|
279
|
+
sha1: match[1].toUpperCase(),
|
|
280
|
+
name: match[2]
|
|
281
|
+
} : null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Whether a profile's application-identifier (TEAM.bundle, maybe ending in *) covers the bundle id. */
|
|
285
|
+
function profileMatchesBundleId(applicationIdentifier, bundleId) {
|
|
286
|
+
if (!applicationIdentifier || !bundleId) return true;
|
|
287
|
+
const appId = applicationIdentifier.slice(applicationIdentifier.indexOf(".") + 1);
|
|
288
|
+
if (appId === "*" || appId === bundleId) return true;
|
|
289
|
+
return appId.endsWith("*") && bundleId.startsWith(appId.slice(0, -1));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Xcode 16+ keeps profiles under UserData; older Xcode (and Xcode 26 still) also reads MobileDevice. */
|
|
293
|
+
function profileDirectories(homeDir = _os.default.homedir()) {
|
|
294
|
+
return [_path.default.join(homeDir, "Library", "Developer", "Xcode", "UserData", "Provisioning Profiles"), _path.default.join(homeDir, "Library", "MobileDevice", "Provisioning Profiles")];
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Turn a failed command into an error that carries neither secret, wherever
|
|
299
|
+
* the tool or Node put it (execFileSync's message holds the whole argv).
|
|
300
|
+
*/
|
|
301
|
+
function scrubbedFailure(error, secrets, prefix) {
|
|
302
|
+
const scrub = text => secrets.reduce((acc, secret) => secret ? acc.split(secret).join("***") : acc, String(text || ""));
|
|
303
|
+
const said = String(error?.stderr || error?.stdout || "").trim().split("\n").filter(Boolean).pop();
|
|
304
|
+
const failure = new Error(scrub(`${prefix}${said ? `: ${said}` : ""}`));
|
|
305
|
+
if (error?.code === "ENOENT") failure.code = "ENOENT";
|
|
306
|
+
return failure;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Read the fields a build needs from a .mobileprovision (a CMS-signed plist).
|
|
311
|
+
*
|
|
312
|
+
* @param {string} file
|
|
313
|
+
* @param {object} options
|
|
314
|
+
* @param {string} options.workDir - a private folder for the decoded plist
|
|
315
|
+
* @param {Function} [options.run]
|
|
316
|
+
*/
|
|
317
|
+
function readProvisioningProfile(file, {
|
|
318
|
+
workDir,
|
|
319
|
+
run = defaultRun
|
|
320
|
+
}) {
|
|
321
|
+
const decoded = _path.default.join(workDir, "profile.plist");
|
|
322
|
+
try {
|
|
323
|
+
run("security", ["cms", "-D", "-i", file, "-o", decoded]);
|
|
324
|
+
} catch (error) {
|
|
325
|
+
throw (0, _utils.nativeError)("IOS-012", `${_path.default.basename(file)} is not a provisioning profile`);
|
|
326
|
+
}
|
|
327
|
+
const extract = keyPath => {
|
|
328
|
+
try {
|
|
329
|
+
return String(run("plutil", ["-extract", keyPath, "raw", "-o", "-", decoded])).trim() || undefined;
|
|
330
|
+
} catch (error) {
|
|
331
|
+
return undefined;
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
const profile = {
|
|
335
|
+
uuid: extract("UUID"),
|
|
336
|
+
name: extract("Name"),
|
|
337
|
+
teamId: extract("TeamIdentifier.0"),
|
|
338
|
+
applicationIdentifier: extract("Entitlements.application-identifier"),
|
|
339
|
+
apsEnvironment: extract("Entitlements.aps-environment"),
|
|
340
|
+
expirationDate: extract("ExpirationDate")
|
|
341
|
+
};
|
|
342
|
+
if (!profile.uuid || !/^[0-9A-Fa-f-]+$/.test(profile.uuid)) {
|
|
343
|
+
throw (0, _utils.nativeError)("IOS-012", "the provisioning profile has no UUID");
|
|
344
|
+
}
|
|
345
|
+
return profile;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Local signing with credentials/ios/: import the .p12 into a temporary
|
|
350
|
+
* keychain, put that keychain on the user search list, and install the
|
|
351
|
+
* .mobileprovision where Xcode looks for profiles.
|
|
352
|
+
*
|
|
353
|
+
* `cleanup()` restores the exact search list read before anything changed,
|
|
354
|
+
* deletes the keychain (and the decoded profile beside it), and removes the
|
|
355
|
+
* profile files this call created. It never touches credentials/ios/. It runs on failure here, and on SIGINT/SIGTERM until the
|
|
356
|
+
* caller has run it. Call it in a `finally`.
|
|
357
|
+
*
|
|
358
|
+
* @param {object} options
|
|
359
|
+
* @param {{ file: string, password?: string, profileFile: string }} options.p12 - from readIosCredentials
|
|
360
|
+
* @param {string} [options.bundleId] - checked against the profile
|
|
361
|
+
* @param {Record<string, string | undefined>} [options.env] - for maskSecret
|
|
362
|
+
* @param {(value: string, options?: object) => boolean} [options.mask]
|
|
363
|
+
* @param {(command: string, args: string[], options?: object) => string} [options.run]
|
|
364
|
+
* @param {string} [options.homeDir]
|
|
365
|
+
* @param {string} [options.tmpRoot]
|
|
366
|
+
* @param {(message: string, type?: string) => void} [options.log]
|
|
367
|
+
* @param {{ once: Function, removeListener: Function }} [options.signals]
|
|
368
|
+
* @param {(code: number) => void} [options.exit]
|
|
369
|
+
* @param {() => Date} [options.now]
|
|
370
|
+
*/
|
|
371
|
+
function setupP12Signing({
|
|
372
|
+
p12,
|
|
373
|
+
bundleId,
|
|
374
|
+
env = process.env,
|
|
375
|
+
mask = _credentials.maskSecret,
|
|
376
|
+
run = defaultRun,
|
|
377
|
+
homeDir = _os.default.homedir(),
|
|
378
|
+
tmpRoot = _os.default.tmpdir(),
|
|
379
|
+
log = () => {},
|
|
380
|
+
signals = process,
|
|
381
|
+
exit = code => process.exit(code),
|
|
382
|
+
now = () => new Date()
|
|
383
|
+
} = {}) {
|
|
384
|
+
const p12Password = p12?.password ?? "";
|
|
385
|
+
const keychainPassword = _crypto.default.randomBytes(24).toString("base64url");
|
|
386
|
+
if (p12Password) mask(p12Password, {
|
|
387
|
+
env
|
|
388
|
+
});
|
|
389
|
+
mask(keychainPassword, {
|
|
390
|
+
env
|
|
391
|
+
});
|
|
392
|
+
const p12Name = _path.default.basename(p12?.file || "the .p12");
|
|
393
|
+
const secrets = [p12Password, keychainPassword].filter(Boolean);
|
|
394
|
+
const exec = (command, args, prefix) => {
|
|
395
|
+
try {
|
|
396
|
+
return run(command, args);
|
|
397
|
+
} catch (error) {
|
|
398
|
+
throw scrubbedFailure(error, secrets, prefix);
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
const workDir = _fs.default.mkdtempSync(_path.default.join(tmpRoot, "catalyst-ios-signing-"));
|
|
402
|
+
_fs.default.chmodSync(workDir, 0o700);
|
|
403
|
+
const keychainPath = _path.default.join(workDir, "catalyst-signing.keychain-db");
|
|
404
|
+
const state = {
|
|
405
|
+
originalList: null,
|
|
406
|
+
keychainCreated: false,
|
|
407
|
+
installed: [],
|
|
408
|
+
createdDirs: []
|
|
409
|
+
};
|
|
410
|
+
let cleaned = false;
|
|
411
|
+
let releaseSignals = () => {};
|
|
412
|
+
function cleanup() {
|
|
413
|
+
if (cleaned) return;
|
|
414
|
+
cleaned = true;
|
|
415
|
+
releaseSignals();
|
|
416
|
+
const step = (what, fn) => {
|
|
417
|
+
try {
|
|
418
|
+
fn();
|
|
419
|
+
} catch (error) {
|
|
420
|
+
log(`Signing cleanup could not ${what}: ${scrubbedFailure(error, secrets, "").message}`, "warning");
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
if (state.originalList) {
|
|
424
|
+
step("restore the keychain search list", () => run("security", ["list-keychains", "-d", "user", "-s", ...state.originalList]));
|
|
425
|
+
}
|
|
426
|
+
if (state.keychainCreated) {
|
|
427
|
+
step("delete the temporary keychain", () => run("security", ["delete-keychain", keychainPath]));
|
|
428
|
+
}
|
|
429
|
+
for (const file of state.installed) step(`remove ${file}`, () => _fs.default.rmSync(file, {
|
|
430
|
+
force: true
|
|
431
|
+
}));
|
|
432
|
+
for (const dir of state.createdDirs.reverse()) {
|
|
433
|
+
step(`remove ${dir}`, () => {
|
|
434
|
+
if (_fs.default.existsSync(dir) && _fs.default.readdirSync(dir).length === 0) _fs.default.rmdirSync(dir);
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
step("remove the temporary signing folder", () => _fs.default.rmSync(workDir, {
|
|
438
|
+
recursive: true,
|
|
439
|
+
force: true
|
|
440
|
+
}));
|
|
441
|
+
}
|
|
442
|
+
releaseSignals = (0, _credentials.cleanupOnSignal)(cleanup, {
|
|
443
|
+
signals,
|
|
444
|
+
exit
|
|
445
|
+
});
|
|
446
|
+
try {
|
|
447
|
+
const p12File = p12.file;
|
|
448
|
+
const profileFile = p12.profileFile;
|
|
449
|
+
|
|
450
|
+
// Read first: whatever happens next, cleanup puts exactly this back.
|
|
451
|
+
state.originalList = parseKeychainList(exec("security", ["list-keychains", "-d", "user"], "Could not read the keychain search list"));
|
|
452
|
+
state.keychainCreated = true;
|
|
453
|
+
exec("security", ["create-keychain", "-p", keychainPassword, keychainPath], "Could not create the temporary keychain");
|
|
454
|
+
exec("security", ["set-keychain-settings", "-lut", String(KEYCHAIN_LOCK_TIMEOUT_SECONDS), keychainPath], "Could not configure the temporary keychain");
|
|
455
|
+
exec("security", ["unlock-keychain", "-p", keychainPassword, keychainPath], "Could not unlock the temporary keychain");
|
|
456
|
+
try {
|
|
457
|
+
exec("security", ["import", p12File, "-k", keychainPath, "-P", p12Password, "-f", "pkcs12", "-T", "/usr/bin/codesign", "-T", "/usr/bin/security"], "security import failed");
|
|
458
|
+
} catch (error) {
|
|
459
|
+
throw (0, _utils.nativeError)("IOS-012", `could not import ${p12Name}; check p12Password in credentials/ios/signing.properties (${error.message})`);
|
|
460
|
+
}
|
|
461
|
+
exec("security", ["set-key-partition-list", "-S", "apple-tool:,apple:,codesign:", "-s", "-k", keychainPassword, keychainPath], "Could not allow codesign to use the imported key");
|
|
462
|
+
exec("security", ["list-keychains", "-d", "user", "-s", keychainPath, ...state.originalList], "Could not add the temporary keychain to the search list");
|
|
463
|
+
const identity = parseIdentity(exec("security", ["find-identity", "-v", "-p", "codesigning", keychainPath], "Could not list the imported identities"));
|
|
464
|
+
if (!identity) {
|
|
465
|
+
throw (0, _utils.nativeError)("IOS-012", `${p12Name} holds no valid code signing identity`);
|
|
466
|
+
}
|
|
467
|
+
const profile = readProvisioningProfile(profileFile, {
|
|
468
|
+
workDir,
|
|
469
|
+
run
|
|
470
|
+
});
|
|
471
|
+
if (profile.expirationDate && new Date(profile.expirationDate) < now()) {
|
|
472
|
+
throw (0, _utils.nativeError)("IOS-012", `the provisioning profile "${profile.name}" expired on ${profile.expirationDate}`);
|
|
473
|
+
}
|
|
474
|
+
if (!profileMatchesBundleId(profile.applicationIdentifier, bundleId)) {
|
|
475
|
+
throw (0, _utils.nativeError)("IOS-012", `the provisioning profile "${profile.name}" is for ${profile.applicationIdentifier}, not ${bundleId}`);
|
|
476
|
+
}
|
|
477
|
+
for (const dir of profileDirectories(homeDir)) {
|
|
478
|
+
if (!_fs.default.existsSync(dir)) {
|
|
479
|
+
// Track each new level, so cleanup removes only what it added.
|
|
480
|
+
let missing = dir;
|
|
481
|
+
const created = [];
|
|
482
|
+
while (!_fs.default.existsSync(missing)) {
|
|
483
|
+
created.unshift(missing);
|
|
484
|
+
missing = _path.default.dirname(missing);
|
|
485
|
+
}
|
|
486
|
+
_fs.default.mkdirSync(dir, {
|
|
487
|
+
recursive: true
|
|
488
|
+
});
|
|
489
|
+
state.createdDirs.push(...created);
|
|
490
|
+
}
|
|
491
|
+
const target = _path.default.join(dir, `${profile.uuid}.mobileprovision`);
|
|
492
|
+
// A profile already there is the developer's own: use it, never remove it.
|
|
493
|
+
if (_fs.default.existsSync(target)) continue;
|
|
494
|
+
_fs.default.copyFileSync(profileFile, target);
|
|
495
|
+
_fs.default.chmodSync(target, 0o600);
|
|
496
|
+
state.installed.push(target);
|
|
497
|
+
}
|
|
498
|
+
log(`Signing with "${identity.name}" and profile "${profile.name}" (${profile.uuid})`, "info");
|
|
499
|
+
return {
|
|
500
|
+
keychainPath,
|
|
501
|
+
identity,
|
|
502
|
+
profile,
|
|
503
|
+
cleanup
|
|
504
|
+
};
|
|
505
|
+
} catch (error) {
|
|
506
|
+
cleanup();
|
|
507
|
+
throw error;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* The ExportOptions.plist contents for `xcodebuild -exportArchive`.
|
|
513
|
+
*
|
|
514
|
+
* manageAppVersionAndBuildNumber is off so the IPA carries exactly the
|
|
515
|
+
* version and build number Catalyst set.
|
|
516
|
+
*
|
|
517
|
+
* @param {object} options
|
|
518
|
+
* @param {string} options.method
|
|
519
|
+
* @param {string} [options.teamId]
|
|
520
|
+
* @param {string} options.signingStyle - "automatic" or "manual"
|
|
521
|
+
* @param {string} [options.bundleId]
|
|
522
|
+
* @param {string} [options.profileName]
|
|
523
|
+
* @param {string} [options.signingCertificate]
|
|
524
|
+
* @returns {Record<string, any>}
|
|
525
|
+
*/
|
|
526
|
+
function buildExportOptions({
|
|
527
|
+
method,
|
|
528
|
+
teamId,
|
|
529
|
+
signingStyle,
|
|
530
|
+
bundleId,
|
|
531
|
+
profileName,
|
|
532
|
+
signingCertificate
|
|
533
|
+
}) {
|
|
534
|
+
const options = {
|
|
535
|
+
method,
|
|
536
|
+
signingStyle,
|
|
537
|
+
destination: "export",
|
|
538
|
+
stripSwiftSymbols: true
|
|
539
|
+
};
|
|
540
|
+
if (teamId) options.teamID = teamId;
|
|
541
|
+
if (signingStyle === "manual") {
|
|
542
|
+
if (bundleId && profileName) options.provisioningProfiles = {
|
|
543
|
+
[bundleId]: profileName
|
|
544
|
+
};
|
|
545
|
+
if (signingCertificate) options.signingCertificate = signingCertificate;
|
|
546
|
+
}
|
|
547
|
+
if (method === "app-store-connect") {
|
|
548
|
+
options.uploadSymbols = true;
|
|
549
|
+
options.manageAppVersionAndBuildNumber = false;
|
|
550
|
+
}
|
|
551
|
+
return options;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Write the export options as an XML plist. plutil does the encoding, so a
|
|
556
|
+
* profile name or team with &, < or quotes comes out escaped.
|
|
557
|
+
*/
|
|
558
|
+
function writeExportOptionsPlist(options, file, {
|
|
559
|
+
run = defaultRun
|
|
560
|
+
} = {}) {
|
|
561
|
+
const json = `${file}.json`;
|
|
562
|
+
_fs.default.writeFileSync(json, JSON.stringify(options), {
|
|
563
|
+
mode: 0o600
|
|
564
|
+
});
|
|
565
|
+
try {
|
|
566
|
+
run("plutil", ["-convert", "xml1", "-o", file, json]);
|
|
567
|
+
} finally {
|
|
568
|
+
_fs.default.rmSync(json, {
|
|
569
|
+
force: true
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
return file;
|
|
573
|
+
}
|
|
574
|
+
const SIGNING_FAILURE_PATTERNS = [/requires a development team/i, /No Accounts?\b/i, /No Account for Team/i, /No profiles? for ['"].*['"] (?:were|was) found/i, /No signing certificate/i, /requires a provisioning profile/i, /Provisioning profile ".*" (?:doesn't|does not|is not|has expired)/i, /doesn't match the entitlements file's value/i, /errSecInternalComponent/i, /Code ?Sign(?:ing)? error/i, /Automatic signing is (?:disabled|unable)/i, /authentication ?key/i, /Failed Registering Bundle Identifier/i];
|
|
575
|
+
|
|
576
|
+
/** True when xcodebuild failed because of signing rather than the code. */
|
|
577
|
+
function isSigningFailure(output) {
|
|
578
|
+
const text = String(output || "");
|
|
579
|
+
return SIGNING_FAILURE_PATTERNS.some(pattern => pattern.test(text));
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* The coded error for a signing failure, keeping xcodebuild's transcript for the diagnostic.
|
|
584
|
+
*
|
|
585
|
+
* @param {string} detail
|
|
586
|
+
* @param {any} [cause]
|
|
587
|
+
* @returns {any}
|
|
588
|
+
*/
|
|
589
|
+
function signingSetupError(detail, cause) {
|
|
590
|
+
const error = (0, _utils.nativeError)("IOS-010", detail);
|
|
591
|
+
if (cause) {
|
|
592
|
+
error.cause = cause;
|
|
593
|
+
if (cause.output) error.output = cause.output;
|
|
594
|
+
}
|
|
595
|
+
return error;
|
|
596
|
+
}
|