crossbind 2.0.0-beta.67 → 2.0.0-beta.69
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/README.md +33 -24
- package/package.json +69 -59
- package/src/actions/addExtension.js +191 -0
- package/src/actions/androidGradle.js +69 -25
- package/src/actions/appInstaller.js +69 -153
- package/src/actions/appSwift.js +66 -0
- package/src/actions/appleSigning.js +32 -14
- package/src/actions/buildApp.js +229 -67
- package/src/actions/buildNode.js +6 -1
- package/src/actions/buildWasm.js +0 -9
- package/src/actions/createInterface.js +17 -8
- package/src/actions/createLib.js +5 -7
- package/src/actions/extensionApi.js +298 -0
- package/src/actions/extensionCommands.js +41 -0
- package/src/actions/extensionsInApp.js +19 -0
- package/src/actions/getCmakeParameters.js +2 -3
- package/src/actions/gradleProject.js +199 -16
- package/src/actions/iosDevice.js +200 -0
- package/src/actions/iosSimulator.js +3 -3
- package/src/actions/licenses.js +83 -34
- package/src/actions/linuxPackages.js +48 -0
- package/src/actions/mavenVerification.js +37 -0
- package/src/actions/publish.js +220 -0
- package/src/actions/uploadKey.js +134 -0
- package/src/actions/windowsSigning.js +1 -1
- package/src/actions/xcodeProject.js +63 -17
- package/src/actions/xcodeTargets.js +122 -0
- package/src/actions/xcodebuild.js +65 -0
- package/src/app/index.d.ts +126 -133
- package/src/app/index.js +126 -188
- package/src/assets/configure/install.sh +0 -0
- package/src/assets/toolchain-digests.json +20 -20
- package/src/bin.js +79 -26
- package/src/index.js +5 -1
- package/src/program.js +35 -4
- package/src/state/index.js +2 -1
- package/src/state/loadConfig.js +16 -5
- package/src/utils/appApiHeader.js +4 -0
- package/src/utils/appExtensions.js +761 -0
- package/src/utils/appFiles.js +58 -0
- package/src/utils/appLaunches.js +249 -0
- package/src/utils/appOptions.js +144 -95
- package/src/utils/appTargets.js +168 -0
- package/src/utils/ask.js +38 -0
- package/src/utils/bridgeTools.js +66 -0
- package/src/utils/configChanges.js +30 -0
- package/src/utils/configWriter.js +181 -0
- package/src/utils/extensionSteps.js +135 -0
- package/src/utils/hermes.js +24 -0
- package/src/utils/keychain.js +54 -0
- package/src/utils/openUrl.js +12 -0
- package/src/utils/password.js +34 -0
- package/src/utils/plist.js +5 -0
- package/src/utils/requiredReasonApis.js +77 -0
- package/src/utils/resolveAppHost.js +7 -0
- package/src/utils/secretFile.js +23 -0
- package/src/utils/systemConfig.js +19 -0
- package/src/utils/systemKeys.js +16 -0
- package/vitest.config.js +6 -1
- package/src/actions/appUpdate.js +0 -68
- package/src/actions/extensions.js +0 -7
|
@@ -1,177 +1,93 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
-
import os from 'node:os';
|
|
3
2
|
import path from 'node:path';
|
|
4
|
-
import
|
|
3
|
+
import extensionApi from './extensionApi.js';
|
|
5
4
|
import { appleSigning } from './appleSigning.js';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
5
|
+
import { DESKTOP_PLATFORMS, recordAppOutput } from './buildApp.js';
|
|
6
|
+
import { windowsSigning } from './windowsSigning.js';
|
|
8
7
|
import state from '../state/index.js';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
8
|
+
import { hasTerminal } from '../utils/ask.js';
|
|
9
|
+
import { appInstallers } from '../utils/appOptions.js';
|
|
10
|
+
import {
|
|
11
|
+
extensionStep, ownExtensions, withListed, withoutNamed,
|
|
12
|
+
} from '../utils/appExtensions.js';
|
|
13
13
|
import resolveAppHostRoot from '../utils/resolveAppHost.js';
|
|
14
14
|
|
|
15
|
-
const
|
|
16
|
-
// What a Linux app runs on: WebKitGTK 4.1 from its 2.40 release, and the GTK its window is.
|
|
17
|
-
const DEBIAN_DEPENDS = 'libwebkit2gtk-4.1-0 (>= 2.40), libgtk-3-0';
|
|
15
|
+
const listed = (names) => (names.length > 1 ? `${names.slice(0, -1).join(', ')} and ${names.at(-1)}` : names[0]);
|
|
18
16
|
|
|
19
|
-
|
|
17
|
+
// crossbind's own installers, which the host brings as it brings its other extensions.
|
|
18
|
+
export const builtInInstallers = () => withoutNamed(ownExtensions(resolveAppHostRoot()), state.config?.app).filter(({ installer }) => installer);
|
|
20
19
|
|
|
21
|
-
// The
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
20
|
+
// The installers an app has: crossbind's own, but for those whose role the app's extensions take, each in the place of the
|
|
21
|
+
// app's extension of its name that has an installer, then those of the app's other extensions.
|
|
22
|
+
export function installersOf(extensions) {
|
|
23
|
+
const taken = extensions.flatMap(({ replaces }) => replaces ?? []);
|
|
24
|
+
return withListed(builtInInstallers().filter(({ name }) => !taken.includes(name)), extensions.filter((extension) => extension.installer));
|
|
26
25
|
}
|
|
27
26
|
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
export function
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
|
|
27
|
+
// The installers of the target's platform that make what installs its app: every one, or those app.installers names,
|
|
28
|
+
// which names none that is no installer, and those that run last whatever it names.
|
|
29
|
+
export function installersFor(target) {
|
|
30
|
+
const all = installersOf(state.config.extensions ?? []);
|
|
31
|
+
const named = appInstallers(state.config);
|
|
32
|
+
const unknown = named?.find((name) => !all.some((extension) => extension.name === name));
|
|
33
|
+
if (unknown) throw new Error(`crossbind: app.installers names ${unknown}, which is no installer: ${listed(all.filter(({ installer }) => !installer.isLast).map(({ name }) => name))} are.`);
|
|
34
|
+
const platform = DESKTOP_PLATFORMS[target.platform];
|
|
35
|
+
const taken = all.filter(({ name, installer }) => installer.platforms.includes(platform) && (!named || named.includes(name) || installer.isLast));
|
|
36
|
+
return [...taken.filter(({ installer }) => !installer.isLast), ...taken.filter(({ installer }) => installer.isLast)];
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
export const debianVersion = (version) => version.replace('-', '~');
|
|
40
|
-
|
|
41
|
-
// package.json's author as Debian names a maintainer, by name and email address.
|
|
42
|
-
export function maintainer(author) {
|
|
43
|
-
if (typeof author === 'string') return author.replace(/\(.*\)/, '').trim() || null;
|
|
44
|
-
if (!author?.name) return null;
|
|
45
|
-
return author.email ? `${author.name} <${author.email}>` : author.name;
|
|
46
|
-
}
|
|
39
|
+
const apiOf = (name, target) => extensionApi(name, { platform: DESKTOP_PLATFORMS[target.platform], isGuided: hasTerminal(), target });
|
|
47
40
|
|
|
48
|
-
// The
|
|
49
|
-
|
|
50
|
-
export function debianControl({ name, version, arch, author, description, label }) {
|
|
51
|
-
return [
|
|
52
|
-
`Package: ${name}`,
|
|
53
|
-
`Version: ${debianVersion(version)}`,
|
|
54
|
-
`Architecture: ${DEBIAN_ARCH[arch]}`,
|
|
55
|
-
`Maintainer: ${maintainer(author) ?? label}`,
|
|
56
|
-
`Depends: ${DEBIAN_DEPENDS}`,
|
|
57
|
-
`Description: ${(description || label).replace(/\s+/g, ' ').trim()}`,
|
|
58
|
-
'',
|
|
59
|
-
].join('\n');
|
|
60
|
-
}
|
|
41
|
+
// The installers an extension replaces, told once a run, which checks them for each of its targets.
|
|
42
|
+
const toldReplacing = new Set();
|
|
61
43
|
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const image = app.replace(/\.app$/, '.dmg');
|
|
79
|
-
const folder = fs.mkdtempSync(path.join(os.tmpdir(), 'crossbind-dmg-'));
|
|
80
|
-
try {
|
|
81
|
-
// The image's root takes the folder's permissions, which mkdtemp keeps to its owner.
|
|
82
|
-
fs.chmodSync(folder, 0o755);
|
|
83
|
-
fs.cpSync(app, path.join(folder, path.basename(app)), { recursive: true, verbatimSymlinks: true });
|
|
84
|
-
fs.symlinkSync('/Applications', path.join(folder, 'Applications'));
|
|
85
|
-
fs.rmSync(image, { force: true });
|
|
86
|
-
run(null, ['hdiutil', 'create', '-quiet', '-volname', appDisplayName(state.config), '-srcfolder', folder, '-fs', 'HFS+', '-format', 'UDZO', image], null, target);
|
|
87
|
-
} finally {
|
|
88
|
-
fs.rmSync(folder, { recursive: true, force: true });
|
|
44
|
+
// What a desktop app ships with stops a build that could not make it here, before anything compiles: the keys that sign
|
|
45
|
+
// the app, the installers app.installers names, and what each installer checks, which it does knowing those that run.
|
|
46
|
+
export async function checkInstaller(target) {
|
|
47
|
+
if (!DESKTOP_PLATFORMS[target.platform]) return;
|
|
48
|
+
if (target.platform === 'darwin') appleSigning();
|
|
49
|
+
if (target.platform === 'win32') windowsSigning();
|
|
50
|
+
const installers = installersFor(target);
|
|
51
|
+
const own = builtInInstallers().map(({ name }) => name);
|
|
52
|
+
installers.filter((extension) => own.includes(extension.name) && state.config.extensions?.includes(extension) && !toldReplacing.has(extension.name))
|
|
53
|
+
.forEach(({ name, root }) => {
|
|
54
|
+
toldReplacing.add(name);
|
|
55
|
+
console.log(`crossbind: the extension ${name} of ${root} replaces crossbind's own installer of its name.`);
|
|
56
|
+
});
|
|
57
|
+
const names = Object.freeze(installers.map(({ name }) => name));
|
|
58
|
+
for (const { name, installer } of installers) {
|
|
59
|
+
if (installer.check) await extensionStep(name, 'check', () => installer.check(apiOf(name, target), Object.freeze({ installers: names })));
|
|
89
60
|
}
|
|
90
61
|
}
|
|
91
62
|
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const { general, paths } = state.config;
|
|
97
|
-
const name = debianName(general.name);
|
|
98
|
-
const label = appDisplayName(state.config);
|
|
99
|
-
const root = `${paths.build}/${path.basename(app)}.deb`;
|
|
100
|
-
const dir = (relative) => {
|
|
101
|
-
fs.mkdirSync(`${root}/${relative}`, { recursive: true });
|
|
102
|
-
return `${root}/${relative}`;
|
|
103
|
-
};
|
|
104
|
-
fs.rmSync(root, { recursive: true, force: true });
|
|
105
|
-
fs.cpSync(app, `${dir('usr/lib')}/${name}`, { recursive: true, verbatimSymlinks: true });
|
|
106
|
-
fs.symlinkSync(`../lib/${name}/${general.name}`, `${dir('usr/bin')}/${name}`);
|
|
107
|
-
const icon = `${app}/resources/icon.png`;
|
|
108
|
-
const hasIcon = fs.existsSync(icon);
|
|
109
|
-
if (hasIcon) fs.copyFileSync(icon, `${dir('usr/share/icons/hicolor/512x512/apps')}/${name}.png`);
|
|
110
|
-
fs.writeFileSync(`${dir('usr/share/applications')}/${name}.desktop`, desktopEntry({
|
|
111
|
-
label, command: name, hasIcon, urlSchemes: appUrlSchemes(state.config), categories: appCategory(state.config)?.linux,
|
|
112
|
-
}));
|
|
113
|
-
const { author, description } = state.config.package ?? {};
|
|
114
|
-
fs.writeFileSync(`${dir('DEBIAN')}/control`, debianControl({ name, version: packageVersion(), arch: target.arch, author, description, label }));
|
|
115
|
-
run(null, ['dpkg-deb', '--build', '--root-owner-group', root, `${app}.deb`], null, target);
|
|
116
|
-
fs.rmSync(root, { recursive: true, force: true });
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// The name a Windows app's setup program gives its Start menu shortcut and folder, which Windows takes as a file name.
|
|
120
|
-
function windowsName() {
|
|
121
|
-
const label = appDisplayName(state.config);
|
|
122
|
-
if (/[<>:/|?*]|[ .]$/.test(label)) {
|
|
123
|
-
throw new Error(`crossbind: ${JSON.stringify(label)}, the name of a Windows app's Start menu shortcut and folder, is no file name Windows takes, which holds none of < > : / | ? * and ends in neither a space nor a dot. Change app.name in crossbind.config.js.`);
|
|
63
|
+
// The files an installer made, by their paths, a relative one beside the app.
|
|
64
|
+
function madeFiles(name, app, files) {
|
|
65
|
+
if (!Array.isArray(files) || !files.every((file) => typeof file === 'string')) {
|
|
66
|
+
throw new Error(`crossbind: the installer ${name} made no list of files, which its make returns.`);
|
|
124
67
|
}
|
|
125
|
-
return
|
|
68
|
+
return files.map((file) => {
|
|
69
|
+
const made = path.resolve(path.dirname(app), file);
|
|
70
|
+
if (!fs.statSync(made, { throwIfNoEntry: false })?.isFile()) throw new Error(`crossbind: the installer ${name} made ${made}, which is no file.`);
|
|
71
|
+
return made;
|
|
72
|
+
});
|
|
126
73
|
}
|
|
127
74
|
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
function windowsSetup(app) {
|
|
137
|
-
const signing = windowsSigning();
|
|
138
|
-
const icon = readAppIcon(state.config);
|
|
139
|
-
const iconFile = `${state.config.paths.build}/${path.basename(app)}.ico`;
|
|
140
|
-
if (icon) fs.writeFileSync(iconFile, windowsIcon(icon));
|
|
141
|
-
const urlSchemes = appUrlSchemes(state.config);
|
|
142
|
-
const schemesFile = `${state.config.paths.build}/${path.basename(app)}.schemes.nsh`;
|
|
143
|
-
if (urlSchemes.length > 0) fs.writeFileSync(schemesFile, schemesMacro(urlSchemes));
|
|
144
|
-
inWindowsImage([
|
|
145
|
-
'makensis', '-V2', '-INPUTCHARSET', 'UTF8',
|
|
146
|
-
`-DNAME=${nsisText(windowsName())}`,
|
|
147
|
-
`-DEXE=${nsisText(`${state.config.general.name}.exe`)}`,
|
|
148
|
-
`-DID=${appId(state.config)}`,
|
|
149
|
-
`-DVERSION=${nsisText(packageVersion())}`,
|
|
150
|
-
`-DAPP=${app}`,
|
|
151
|
-
`-DOUTFILE=${app}-setup.exe`,
|
|
152
|
-
...(icon ? [`-DICON=${iconFile}`] : []),
|
|
153
|
-
...(urlSchemes.length > 0 ? [`-DSCHEMES=${schemesFile}`] : []),
|
|
154
|
-
...(signing ? [`-DSIGNER=${jsignScript()}`] : []),
|
|
155
|
-
`${resolveAppHostRoot()}/windows/installer.nsi`,
|
|
156
|
-
], path.dirname(app), signing);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// What an app's installer takes from the app and the environment, checked before anything compiles: the names, the keys
|
|
160
|
-
// that sign a macOS or Windows app, and the one that signs a desktop app's update.
|
|
161
|
-
export function checkInstaller(target) {
|
|
162
|
-
if (target.platform === 'darwin') appleSigning();
|
|
163
|
-
else if (target.platform === 'linux') debianName(state.config.general.name);
|
|
164
|
-
else if (target.platform === 'win32') {
|
|
165
|
-
windowsName();
|
|
166
|
-
windowsSigning();
|
|
75
|
+
// What installs a desktop app, beside it: each installer's that made any, by its name, with the files it made, in the
|
|
76
|
+
// order they run, each seeing what those before it made.
|
|
77
|
+
export async function writeInstallers(app, target) {
|
|
78
|
+
const made = [];
|
|
79
|
+
for (const { name, installer } of installersFor(target)) {
|
|
80
|
+
const files = await extensionStep(name, 'make', () => installer.make(apiOf(name, target), Object.freeze({ app, arch: target.arch, made: structuredClone(made) })));
|
|
81
|
+
const madeNow = madeFiles(name, app, files);
|
|
82
|
+
if (madeNow.length > 0) made.push({ name, files: madeNow });
|
|
167
83
|
}
|
|
168
|
-
|
|
84
|
+
return made;
|
|
169
85
|
}
|
|
170
86
|
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
export
|
|
174
|
-
if (target.platform
|
|
175
|
-
|
|
176
|
-
|
|
87
|
+
// A desktop app with its page ships with what installs it. Installers write beside the app, in the page's own folder, so
|
|
88
|
+
// they run once every app of the build has its page.
|
|
89
|
+
export async function shipApp(target, to) {
|
|
90
|
+
if (!DESKTOP_PLATFORMS[target.platform]) return;
|
|
91
|
+
const app = path.resolve(to);
|
|
92
|
+
recordAppOutput(target, app, { installers: await writeInstallers(app, target) });
|
|
177
93
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { DARWIN_DEPLOYMENT_TARGET } from '../utils/darwinToolchain.js';
|
|
5
|
+
import { IOS_DEPLOYMENT_TARGET } from '../utils/iosToolchain.js';
|
|
6
|
+
|
|
7
|
+
// An Apple app's extensions' Swift, which CMake's Makefiles generator compiles none of: swiftc makes each extension's a
|
|
8
|
+
// static library first, with the Objective-C header its own sources import, before the host builds.
|
|
9
|
+
|
|
10
|
+
// The module an extension's Swift makes: its name, with underscores for dashes, as a Swift module's name takes it.
|
|
11
|
+
export const swiftModule = (name) => name.replace(/-/g, '_');
|
|
12
|
+
|
|
13
|
+
// What swiftc compiles an Apple target's Swift for: the target's triple, at the app's deployment target, and its SDK.
|
|
14
|
+
export function swiftTarget(target) {
|
|
15
|
+
if (target.platform === 'darwin') return { triple: `${target.arch === 'x64' ? 'x86_64' : 'arm64'}-apple-macos${DARWIN_DEPLOYMENT_TARGET}`, sdk: 'macosx' };
|
|
16
|
+
const isDevice = target.arch === 'iphoneos';
|
|
17
|
+
return { triple: `arm64-apple-ios${IOS_DEPLOYMENT_TARGET}${isDevice ? '' : '-simulator'}`, sdk: isDevice ? 'iphoneos' : 'iphonesimulator' };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// The swiftc arguments that make one extension's Swift a static library and the Objective-C header of its @objc classes
|
|
21
|
+
// and @_cdecl functions.
|
|
22
|
+
export function swiftArgs({ module, sources, library, header, triple, sdkPath, isRelease }) {
|
|
23
|
+
return [
|
|
24
|
+
'swiftc', '-parse-as-library', ...(isRelease ? ['-O'] : ['-Onone', '-g']), '-module-name', module,
|
|
25
|
+
'-emit-library', '-static', '-o', library, '-emit-objc-header', '-emit-objc-header-path', header,
|
|
26
|
+
'-target', triple, '-sdk', sdkPath, ...sources,
|
|
27
|
+
];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function xcrun(args, what) {
|
|
31
|
+
const result = spawnSync('xcrun', args, { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
|
|
32
|
+
if (result.error) throw new Error(`crossbind: ${what} needs Xcode's xcrun: ${result.error.message}`);
|
|
33
|
+
if (result.status !== 0) throw new Error(`crossbind: ${what} failed:\n${`${result.stdout}${result.stderr}`.trim()}`);
|
|
34
|
+
return result.stdout.trim();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Compiles each extension's Swift for an Apple target into its folder of workDir: the library, and the header beside it,
|
|
38
|
+
// which the folder, added to the extension's includes, gives its sources as <module>-Swift.h.
|
|
39
|
+
export function compileSwift(target, compiled, workDir, isRelease) {
|
|
40
|
+
const withSwift = compiled.filter(({ swift }) => swift.length > 0);
|
|
41
|
+
if (withSwift.length === 0) return [];
|
|
42
|
+
const { triple, sdk } = swiftTarget(target);
|
|
43
|
+
const sdkPath = xcrun(['--sdk', sdk, '--show-sdk-path'], `the ${sdk} SDK's path`);
|
|
44
|
+
return withSwift.map(({ name, swift }) => {
|
|
45
|
+
const module = swiftModule(name);
|
|
46
|
+
const dir = path.join(workDir, 'swift', module);
|
|
47
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
48
|
+
const library = path.join(dir, `lib${module}.a`);
|
|
49
|
+
const header = path.join(dir, `${module}-Swift.h`);
|
|
50
|
+
xcrun(swiftArgs({ module, sources: swift, library, header, triple, sdkPath, isRelease }), `the extension ${name}'s Swift`);
|
|
51
|
+
return { name, library, includes: [dir] };
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// What links the libraries: each whole, as an @objc class may be reached by its name alone, and Swift's own libraries,
|
|
56
|
+
// which the system holds on the app's deployment targets, beside the toolchain's back-deployed parts. An iOS app's
|
|
57
|
+
// Xcode project names them by its own settings, the back-deployed parts in Xcode's default toolchain, whose swiftc
|
|
58
|
+
// compiled them: TOOLCHAIN_DIR may name another, as the Metal toolchain.
|
|
59
|
+
export function swiftLinkArgs(target, libraries) {
|
|
60
|
+
if (libraries.length === 0) return [];
|
|
61
|
+
const searched = target.platform === 'darwin'
|
|
62
|
+
? [`-L${xcrun(['--sdk', 'macosx', '--show-sdk-path'], "the macosx SDK's path")}/usr/lib/swift`,
|
|
63
|
+
`-L${path.dirname(path.dirname(xcrun(['--find', 'swiftc'], "Swift's compiler")))}/lib/swift/macosx`]
|
|
64
|
+
: ['-L$(SDKROOT)/usr/lib/swift', '-L$(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME)'];
|
|
65
|
+
return [...libraries.map((library) => `-Wl,-force_load,${library}`), ...searched, '-Wl,-rpath,/usr/lib/swift'];
|
|
66
|
+
}
|
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { secretFile } from '../utils/secretFile.js';
|
|
5
6
|
|
|
6
7
|
// The Developer ID that signs a macOS app and the App Store Connect API key Apple notarizes it with, named by the
|
|
7
8
|
// environment alone so that no secret reaches a file.
|
|
@@ -12,6 +13,10 @@ export const APPLE_SIGNING = {
|
|
|
12
13
|
issuer: 'CROSSBIND_APPLE_API_ISSUER',
|
|
13
14
|
};
|
|
14
15
|
const NOTARY = [APPLE_SIGNING.key, APPLE_SIGNING.keyId, APPLE_SIGNING.issuer];
|
|
16
|
+
// App Store Connect gives a key's file as AuthKey_<key id>.p8, so the file's own name tells its ID.
|
|
17
|
+
const KEY_FILE = /^AuthKey_([A-Z0-9]{10})\.p8$/;
|
|
18
|
+
// A key's own text, as CI's secrets give it, in place of its file's path.
|
|
19
|
+
const KEY_TEXT = /^\s*-----BEGIN /;
|
|
15
20
|
|
|
16
21
|
const listed = (names) => `${names.join(' and ')} ${names.length > 1 ? 'are' : 'is'}`;
|
|
17
22
|
|
|
@@ -30,16 +35,32 @@ function signingIdentities() {
|
|
|
30
35
|
.map(([, hash, name]) => ({ hash, name }));
|
|
31
36
|
}
|
|
32
37
|
|
|
38
|
+
// The App Store Connect API key the environment names, with which Apple notarizes a macOS app and xcodebuild uploads an
|
|
39
|
+
// iOS one in place of Xcode's account, or null: all of it or none, but for its ID where its file's name tells it. The key
|
|
40
|
+
// is its file, or its text, which goes to a file as the tools take files.
|
|
41
|
+
export function appStoreKey(env = process.env) {
|
|
42
|
+
const set = NOTARY.filter((name) => env[name]);
|
|
43
|
+
if (!set.length) return null;
|
|
44
|
+
const given = env[APPLE_SIGNING.key] ?? '';
|
|
45
|
+
const isText = KEY_TEXT.test(given);
|
|
46
|
+
const keyId = env[APPLE_SIGNING.keyId] || (isText ? undefined : path.basename(given).match(KEY_FILE)?.[1]);
|
|
47
|
+
const missing = NOTARY.filter((name) => !env[name] && !(name === APPLE_SIGNING.keyId && keyId));
|
|
48
|
+
if (missing.length) {
|
|
49
|
+
const byName = missing.includes(APPLE_SIGNING.keyId) && !isText ? ', the ID unless its file keeps the name App Store Connect gives it, AuthKey_<key id>.p8' : '';
|
|
50
|
+
throw new Error(`crossbind: ${listed(set)} set, but ${listed(missing)} not: an App Store Connect API key takes all three${byName}.`);
|
|
51
|
+
}
|
|
52
|
+
const key = isText ? secretFile('AuthKey.p8', `${given.trim()}\n`) : path.resolve(given);
|
|
53
|
+
if (!fs.statSync(key, { throwIfNoEntry: false })?.isFile()) throw new Error(`crossbind: ${APPLE_SIGNING.key} names ${key}, which is no file.`);
|
|
54
|
+
return { key, keyId, issuer: env[APPLE_SIGNING.issuer] };
|
|
55
|
+
}
|
|
56
|
+
|
|
33
57
|
// The identity a macOS app is signed with and the key Apple notarizes it with, or null without an identity: the app is
|
|
34
|
-
// then signed ad hoc, which opens it on the Mac that built it alone
|
|
35
|
-
// renewed certificates of one name do not share.
|
|
58
|
+
// then signed ad hoc, which opens it on the Mac that built it alone, even with the key, which iOS uploads take too.
|
|
59
|
+
// codesign takes the identity's hash, which the renewed certificates of one name do not share.
|
|
36
60
|
export function appleSigning(env = process.env) {
|
|
37
|
-
const notary = NOTARY.filter((name) => env[name]);
|
|
38
61
|
const identity = env[APPLE_SIGNING.identity];
|
|
39
|
-
if (!identity)
|
|
40
|
-
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
62
|
+
if (!identity) return null;
|
|
63
|
+
const notary = appStoreKey(env);
|
|
43
64
|
const found = signingIdentities().filter(({ hash, name }) => hash === identity.toUpperCase() || name.includes(identity));
|
|
44
65
|
const matches = [...new Map(found.map((match) => [match.hash, match])).values()];
|
|
45
66
|
if (!matches.length) throw new Error(`crossbind: ${APPLE_SIGNING.identity} names ${JSON.stringify(identity)}, which no valid code signing identity in the keychain is; \`security find-identity -v -p codesigning\` lists them.`);
|
|
@@ -47,13 +68,10 @@ export function appleSigning(env = process.env) {
|
|
|
47
68
|
throw new Error(`crossbind: ${APPLE_SIGNING.identity} names ${JSON.stringify(identity)}, which ${matches.length} identities in the keychain are: ${matches.map(({ hash, name }) => `${hash} "${name}"`).join(', ')}. Set it to one of their hashes.`);
|
|
48
69
|
}
|
|
49
70
|
const [{ hash, name }] = matches;
|
|
50
|
-
if (
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const key = path.resolve(env[APPLE_SIGNING.key]);
|
|
55
|
-
if (!fs.statSync(key, { throwIfNoEntry: false })?.isFile()) throw new Error(`crossbind: ${APPLE_SIGNING.key} names ${key}, which is no file.`);
|
|
56
|
-
return { identity: hash, notary: { key, keyId: env[APPLE_SIGNING.keyId], issuer: env[APPLE_SIGNING.issuer] } };
|
|
71
|
+
if (notary && !name.startsWith('Developer ID Application:')) {
|
|
72
|
+
throw new Error(`crossbind: Apple notarizes an app signed with a Developer ID Application certificate, which ${JSON.stringify(name)} is not.`);
|
|
73
|
+
}
|
|
74
|
+
return { identity: hash, notary };
|
|
57
75
|
}
|
|
58
76
|
|
|
59
77
|
// The ticket Apple issues can take a moment to reach the servers stapler asks for it.
|