crossbind 2.0.0-beta.68 → 2.0.0-beta.72
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 +1 -1
- package/package.json +70 -59
- package/src/actions/addExtension.js +196 -0
- package/src/actions/androidGradle.js +26 -17
- package/src/actions/appInstaller.js +69 -153
- package/src/actions/appSwift.js +66 -0
- package/src/actions/buildApp.js +177 -74
- package/src/actions/buildDependencies.js +3 -3
- package/src/actions/buildNode.js +6 -1
- package/src/actions/buildWasm.js +0 -9
- package/src/actions/createImportedBridges.js +10 -8
- package/src/actions/createInterface.js +40 -29
- package/src/actions/createLib.js +3 -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/getAllBridges.js +20 -5
- package/src/actions/getDependLibs.js +5 -0
- package/src/actions/getLinkInputs.js +2 -1
- package/src/actions/gradleProject.js +207 -16
- package/src/actions/iosDevice.js +2 -2
- package/src/actions/iosSimulator.js +3 -3
- package/src/actions/licenses.js +113 -58
- package/src/actions/linuxPackages.js +48 -0
- package/src/actions/mavenVerification.js +37 -0
- package/src/actions/{prepareConanDependencies.js → preparePackageSources.js} +13 -12
- package/src/actions/publish.js +220 -0
- package/src/actions/windowsSigning.js +1 -1
- package/src/actions/writeRuntimeEntry.js +2 -2
- package/src/actions/xcodeProject.js +60 -29
- package/src/actions/xcodeTargets.js +122 -0
- package/src/actions/xcodebuild.js +65 -0
- package/src/app/index.d.ts +144 -132
- package/src/app/index.js +147 -185
- package/src/assets/configure/install.sh +0 -0
- package/src/assets/node-import-hooks.mjs +7 -6
- package/src/assets/toolchain-digests.json +20 -20
- package/src/bin.js +37 -37
- package/src/index.js +4 -3
- package/src/integration/getCrossbindScript.js +3 -2
- package/src/integration/getDependFilePath.js +10 -10
- package/src/program.js +32 -3
- package/src/state/{attachConanDependencies.js → attachPackageSources.js} +20 -16
- package/src/state/index.js +8 -6
- package/src/state/loadConfig.js +26 -9
- package/src/state/refreshPackageSources.js +19 -0
- package/src/utils/appExtensions.js +777 -0
- package/src/utils/appFiles.js +58 -0
- package/src/utils/appLaunches.js +249 -0
- package/src/utils/appOptions.js +142 -112
- package/src/utils/appRustCrates.js +2 -2
- package/src/utils/appTargets.js +168 -0
- package/src/utils/bridgeTools.js +71 -0
- package/src/utils/configChanges.js +30 -0
- package/src/utils/configWriter.js +143 -26
- package/src/utils/cppDts.js +11 -11
- package/src/utils/extensionSteps.js +172 -0
- package/src/utils/nativeImports.js +9 -2
- package/src/utils/nodeDevBuild.js +2 -1
- package/src/utils/openUrl.js +2 -1
- package/src/utils/packageSourceApi.js +87 -0
- package/src/utils/packageSourceImport.js +69 -0
- package/src/utils/packageSourceInstall.js +162 -0
- package/src/utils/{conanStage.js → packageSourceStage.js} +64 -90
- package/src/utils/packageSources.js +108 -0
- package/src/utils/plist.js +5 -0
- package/src/utils/printableText.js +5 -0
- package/src/utils/resolveAppHost.js +7 -0
- package/src/utils/runPackageSource.js +251 -0
- package/src/utils/rustBridgeGen.js +3 -3
- package/src/utils/systemConfig.js +16 -3
- package/src/utils/targetToolchain.js +77 -0
- package/vitest.config.js +6 -1
- package/src/actions/appSetup.js +0 -116
- package/src/actions/appStoreConnect.js +0 -188
- package/src/actions/appUpdate.js +0 -68
- package/src/actions/extensions.js +0 -7
- package/src/actions/googlePlay.js +0 -213
- package/src/actions/googleSignIn.js +0 -78
- package/src/actions/playSetup.js +0 -19
- package/src/actions/publishAndroid.js +0 -102
- package/src/actions/publishIos.js +0 -146
- package/src/state/refreshConanDependencies.js +0 -17
- package/src/utils/conanDependencies.js +0 -66
- package/src/utils/conanImport.js +0 -58
- package/src/utils/conanInstall.js +0 -154
- package/src/utils/conanProfile.js +0 -203
- package/src/utils/runConan.js +0 -257
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import state from '../state/index.js';
|
|
3
|
-
import { SIGNING } from './androidGradle.js';
|
|
4
|
-
import {
|
|
5
|
-
androidAppId, androidVersion, appBuildNumber, appOutputOf, clearAppOutputs,
|
|
6
|
-
} from './buildApp.js';
|
|
7
|
-
import {
|
|
8
|
-
PLAY, highestVersionCode, keptSignIn, playCredentials, uploadBundle,
|
|
9
|
-
} from './googlePlay.js';
|
|
10
|
-
import { signInToPlay } from './playSetup.js';
|
|
11
|
-
import { askAppId } from './appSetup.js';
|
|
12
|
-
import { makeUploadKey, releaseSigningEnv } from './uploadKey.js';
|
|
13
|
-
import { askLine, askYesNo, hasTerminal } from '../utils/ask.js';
|
|
14
|
-
import loadSystemConfig from '../utils/systemConfig.js';
|
|
15
|
-
import openUrl from '../utils/openUrl.js';
|
|
16
|
-
|
|
17
|
-
const PLAY_CONSOLE = 'https://play.google.com/console';
|
|
18
|
-
const NO_APP_ID = 'crossbind: Google Play knows an app by its package name, app.id, which crossbind.config.js does not set: set it, as com.example.myapp, before the first publish, as Play keeps it for good.';
|
|
19
|
-
const NO_UPLOAD_KEY = `crossbind: Google Play takes bundles signed with your upload key: crossbind app upload-key makes one, or ${SIGNING.keystore}, ${SIGNING.alias} and ${SIGNING.password} name yours.`;
|
|
20
|
-
const NO_CREDENTIALS = `crossbind: Google Play takes uploads from your Google account, which a publish on a terminal signs in to, or from a service account: set ${PLAY.serviceAccount} to its e-mail, with gcloud signed in, ${PLAY.key} to its JSON key, or ${PLAY.token} to an access token of it; app.md's Publishing says how to make one.`;
|
|
21
|
-
const SIGN_IN = 'Google Play takes uploads from your Google account once crossbind is signed in to it. Sign in now, in your browser? [Y/n] ';
|
|
22
|
-
const ANOTHER_ACCOUNT = 'Sign in with another Google account? [Y/n] ';
|
|
23
|
-
|
|
24
|
-
// The highest versionCode Play has seen, and the credentials that saw it, once the user has done what Play asks for,
|
|
25
|
-
// which only the user can do: in Play Console, which crossbind opens before it waits for Enter and checks again, or, for
|
|
26
|
-
// a Google account without release rights, by signing in with another.
|
|
27
|
-
async function highestWithPlayConsole(appId, credentials) {
|
|
28
|
-
let current = credentials;
|
|
29
|
-
for (;;) {
|
|
30
|
-
try {
|
|
31
|
-
return { highest: await highestVersionCode(appId, current), credentials: current };
|
|
32
|
-
} catch (error) {
|
|
33
|
-
console.log(error.message);
|
|
34
|
-
if (error.status === 403 && current.refreshToken) {
|
|
35
|
-
current = await signInToPlay(ANOTHER_ACCOUNT);
|
|
36
|
-
if (!current) throw error;
|
|
37
|
-
} else {
|
|
38
|
-
if ([403, 404].includes(error.status)) {
|
|
39
|
-
console.log(`crossbind: opening ${PLAY_CONSOLE}.`);
|
|
40
|
-
openUrl(PLAY_CONSOLE);
|
|
41
|
-
}
|
|
42
|
-
await askLine('Press Enter once that is done to check again, or Ctrl-C to stop. ');
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Builds an Android app with the next versionCode Google Play takes, signed with the upload key, and releases its bundle
|
|
49
|
-
// on a track. All it needs is checked before the build, which takes minutes: on a terminal, what is missing is asked for
|
|
50
|
-
// and set up; without one, as in CI, every missing piece is named at once.
|
|
51
|
-
export async function publishAndroid({ track, status, viteArgs }, { runVite }) {
|
|
52
|
-
const isGuided = hasTerminal();
|
|
53
|
-
const missing = [];
|
|
54
|
-
let appId = state.config.app.id === undefined ? null : androidAppId();
|
|
55
|
-
if (!appId && isGuided) appId = await askAppId('Google Play', 'package name');
|
|
56
|
-
if (!appId) missing.push(NO_APP_ID);
|
|
57
|
-
|
|
58
|
-
let signing = await releaseSigningEnv();
|
|
59
|
-
if (!signing.isSigned && isGuided && await askYesNo('This machine has no upload key, which Google Play takes bundles signed with. Make one now? [Y/n] ')) {
|
|
60
|
-
await makeUploadKey();
|
|
61
|
-
state.config.system = loadSystemConfig();
|
|
62
|
-
signing = await releaseSigningEnv();
|
|
63
|
-
}
|
|
64
|
-
if (!signing.isSigned) missing.push(NO_UPLOAD_KEY);
|
|
65
|
-
|
|
66
|
-
let credentials = playCredentials(process.env, state.config.system) ?? await keptSignIn();
|
|
67
|
-
if (!credentials && isGuided) credentials = await signInToPlay(SIGN_IN);
|
|
68
|
-
if (!credentials) missing.push(NO_CREDENTIALS);
|
|
69
|
-
if (missing.length > 0) throw new Error(missing.join('\n'));
|
|
70
|
-
const version = state.config.package?.version ?? '1.0.0';
|
|
71
|
-
console.log(`crossbind: publishing ${appId} ${version} to Google Play's ${track} track.`);
|
|
72
|
-
|
|
73
|
-
let highest;
|
|
74
|
-
if (isGuided) ({ highest, credentials } = await highestWithPlayConsole(appId, credentials));
|
|
75
|
-
else highest = await highestVersionCode(appId, credentials);
|
|
76
|
-
const versionCode = appBuildNumber() ?? Math.max(androidVersion().code, highest + 1);
|
|
77
|
-
console.log(highest
|
|
78
|
-
? `crossbind: Google Play has seen versionCode ${highest} at most, so this build is ${versionCode}.`
|
|
79
|
-
: `crossbind: Google Play has no build of ${appId} yet, so this build is ${versionCode}.`);
|
|
80
|
-
|
|
81
|
-
clearAppOutputs();
|
|
82
|
-
const exitCode = await runVite(['build', ...viteArgs], {
|
|
83
|
-
...process.env, ...signing.env, CROSSBIND_APP_PLATFORM: 'android', CROSSBIND_APP_BUILD_NUMBER: String(versionCode),
|
|
84
|
-
});
|
|
85
|
-
if (exitCode !== 0) throw Object.assign(new Error(`crossbind: the build ended with exit code ${exitCode}, so nothing went to Google Play.`), { exitCode });
|
|
86
|
-
const project = appOutputOf('android');
|
|
87
|
-
const aab = `${project}.aab`;
|
|
88
|
-
if (!fs.existsSync(aab)) throw new Error(`crossbind: the build made no bundle at ${aab}, so nothing went to Google Play.`);
|
|
89
|
-
const symbols = fs.existsSync(`${project}-symbols.zip`) ? `${project}-symbols.zip` : null;
|
|
90
|
-
|
|
91
|
-
console.log(`crossbind: uploading the bundle${symbols ? ' and its native debug symbols' : ''} to Google Play.`);
|
|
92
|
-
const released = await uploadBundle(appId, credentials, { aab, symbols, track, status });
|
|
93
|
-
const app = `${appId} ${version} (versionCode ${released.versionCode})`;
|
|
94
|
-
if (released.status !== 'draft') {
|
|
95
|
-
console.log(`crossbind: ${app} is out on Google Play's ${track} track.`);
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
console.log(`crossbind: ${app} is a draft on Google Play's ${track} track.`);
|
|
99
|
-
if (status !== 'draft') {
|
|
100
|
-
console.log("crossbind: Play takes drafts alone until an app's first release is out: roll it out once in Play Console, under Test and release, and later publishes go out by themselves.");
|
|
101
|
-
}
|
|
102
|
-
}
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
import state from '../state/index.js';
|
|
2
|
-
import { appStoreKey } from './appleSigning.js';
|
|
3
|
-
import {
|
|
4
|
-
appBuildNumber, appOutputOf, appTeam, appVersion, clearAppOutputs,
|
|
5
|
-
} from './buildApp.js';
|
|
6
|
-
import {
|
|
7
|
-
addHermesDsym, archiveIosApp, archivedBinary, archivedPrivacy, uploadIosArchive, xcodeTeams,
|
|
8
|
-
} from './appStoreConnect.js';
|
|
9
|
-
import {
|
|
10
|
-
askAppId, askEncryption, askIcon, askPrivacy,
|
|
11
|
-
} from './appSetup.js';
|
|
12
|
-
import { appEncryption, appId } from '../utils/appOptions.js';
|
|
13
|
-
import { resolveHermesDsym } from '../utils/hermes.js';
|
|
14
|
-
import { unreasonedImports } from '../utils/requiredReasonApis.js';
|
|
15
|
-
import { askLine, askYesNo, hasTerminal } from '../utils/ask.js';
|
|
16
|
-
import loadSystemConfig, { updateSystemConfig } from '../utils/systemConfig.js';
|
|
17
|
-
import openUrl from '../utils/openUrl.js';
|
|
18
|
-
|
|
19
|
-
const NO_APP_ID = 'crossbind: App Store Connect knows an app by its bundle id, app.id, which crossbind.config.js does not set: set it, as com.example.myapp, before the first publish, as Apple keeps it for good.';
|
|
20
|
-
const NO_TEAM = "crossbind: App Store Connect takes uploads from an Apple Developer Program team, which neither app.team in crossbind.config.js nor this machine's XCODE_DEVELOPMENT_TEAM names: set either to its id, which developer.apple.com/account shows under Membership details.";
|
|
21
|
-
const NO_ICON = 'crossbind: App Store Connect takes no iOS app without an icon: set app.icon in crossbind.config.js to a square PNG, at best 1024 × 1024.';
|
|
22
|
-
const NO_ENCRYPTION = 'crossbind: app.encryption is not set, so App Store Connect asks whether this build uses encryption before TestFlight hands it out; set it in crossbind.config.js to answer for every build.';
|
|
23
|
-
const TEAM_ID = /^[A-Z0-9]{10}$/;
|
|
24
|
-
|
|
25
|
-
async function pickTeam(teams) {
|
|
26
|
-
if (teams.length === 1) {
|
|
27
|
-
const [{ id, name }] = teams;
|
|
28
|
-
return await askYesNo(`Upload as team ${id}, ${name}, which Xcode is signed in to? [Y/n] `) ? teams[0] : null;
|
|
29
|
-
}
|
|
30
|
-
console.log('Xcode is signed in to these teams:');
|
|
31
|
-
teams.forEach(({ id, name }, index) => console.log(` ${index + 1}) ${id} ${name}`));
|
|
32
|
-
for (;;) {
|
|
33
|
-
const team = teams[Number((await askLine(`Which one uploads, 1 to ${teams.length}? `)).trim()) - 1];
|
|
34
|
-
if (team) return team;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// A team id the user types, or null once they have signed in to Xcode, for its teams to be read again.
|
|
39
|
-
async function typedTeam() {
|
|
40
|
-
console.log("crossbind: Xcode is signed in to no such team, as a free personal team is none. Sign in to Xcode, under Settings › Accounts, with your membership's Apple Account, or type the team's id, which developer.apple.com/account shows under Membership details.");
|
|
41
|
-
for (;;) {
|
|
42
|
-
const id = (await askLine('Team id, or Enter once Xcode is signed in: ')).trim().toUpperCase();
|
|
43
|
-
if (!id) return null;
|
|
44
|
-
if (TEAM_ID.test(id)) return id;
|
|
45
|
-
console.log('That is no team id, which is ten letters and digits.');
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// The team the user picks of those Xcode is signed in to, or types, which this machine keeps as XCODE_DEVELOPMENT_TEAM.
|
|
50
|
-
async function askTeam() {
|
|
51
|
-
console.log('crossbind: App Store Connect takes uploads from an Apple Developer Program team, which neither app.team nor XCODE_DEVELOPMENT_TEAM names.');
|
|
52
|
-
let id = null;
|
|
53
|
-
while (!id) {
|
|
54
|
-
const teams = xcodeTeams();
|
|
55
|
-
if (teams.length > 0) {
|
|
56
|
-
const team = await pickTeam(teams);
|
|
57
|
-
if (!team) throw new Error(NO_TEAM);
|
|
58
|
-
({ id } = team);
|
|
59
|
-
} else {
|
|
60
|
-
id = await typedTeam();
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
updateSystemConfig({ XCODE_DEVELOPMENT_TEAM: id });
|
|
64
|
-
state.config.system = loadSystemConfig();
|
|
65
|
-
if (appTeam() !== id) throw new Error(`crossbind: ~/.crossbind.json did not take XCODE_DEVELOPMENT_TEAM: set app.team: '${id}' in crossbind.config.js.`);
|
|
66
|
-
console.log(`crossbind: ~/.crossbind.json keeps ${id} as XCODE_DEVELOPMENT_TEAM.`);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// A step that failed for what only the user can mend, in Xcode or on Apple's sites, runs again once they press Enter,
|
|
70
|
-
// after crossbind opened the page that mends it where it knows one. Any other failure, or one without a terminal, stops
|
|
71
|
-
// the publish.
|
|
72
|
-
async function untilDone(isGuided, step) {
|
|
73
|
-
for (;;) {
|
|
74
|
-
try {
|
|
75
|
-
return step();
|
|
76
|
-
} catch (error) {
|
|
77
|
-
if (!isGuided || !error.isMendable) throw error;
|
|
78
|
-
console.log(error.message);
|
|
79
|
-
if (error.page) {
|
|
80
|
-
console.log(`crossbind: opening ${error.page}.`);
|
|
81
|
-
openUrl(error.page);
|
|
82
|
-
}
|
|
83
|
-
await askLine('Press Enter once that is done to try again, or Ctrl-C to stop. ');
|
|
84
|
-
console.log('crossbind: trying again, which can take a few minutes.');
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
async function buildIosApp(viteArgs, runVite) {
|
|
90
|
-
clearAppOutputs();
|
|
91
|
-
const exitCode = await runVite(['build', ...viteArgs], { ...process.env, CROSSBIND_APP_PLATFORM: 'ios' });
|
|
92
|
-
if (exitCode !== 0) throw Object.assign(new Error(`crossbind: the build ended with exit code ${exitCode}, so nothing went to App Store Connect.`), { exitCode });
|
|
93
|
-
return appOutputOf('ios');
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const unreasonedError = (unreasoned) => new Error(`crossbind: App Store Connect refuses an iOS app that calls required reason API without Apple's reasons for it (ITMS-91053), and this app calls ${unreasoned.map(({ category, symbols }) => `${category} (${symbols.join(', ')})`).join(' and ')} with no reason in its privacy manifest. Give app.privacy Apple's reasons, as { fileTimestamp: ['C617.1'] }; Apple's "Describing use of required reason API" says what each covers. Nothing went to App Store Connect.`);
|
|
97
|
-
|
|
98
|
-
// Builds an iOS app, archives it and uploads it to App Store Connect, whose TestFlight hands it out. All it needs is
|
|
99
|
-
// checked before the build, which takes minutes: on a terminal, what is missing is asked for and set up; without one,
|
|
100
|
-
// as in CI, every missing piece is named at once. The archived app then shows what required reason API it calls.
|
|
101
|
-
export async function publishIos({ viteArgs }, { runVite }) {
|
|
102
|
-
if (process.platform !== 'darwin') throw new Error('crossbind: iOS apps build and go to App Store Connect on macOS, with Xcode.');
|
|
103
|
-
const isGuided = hasTerminal();
|
|
104
|
-
const key = appStoreKey();
|
|
105
|
-
const missing = [];
|
|
106
|
-
if (state.config.app.id === undefined) {
|
|
107
|
-
if (isGuided) await askAppId('App Store Connect', 'bundle id');
|
|
108
|
-
else missing.push(NO_APP_ID);
|
|
109
|
-
}
|
|
110
|
-
if (!appTeam()) {
|
|
111
|
-
if (isGuided) await askTeam();
|
|
112
|
-
else missing.push(NO_TEAM);
|
|
113
|
-
}
|
|
114
|
-
if (state.config.app.icon === undefined) {
|
|
115
|
-
if (isGuided) await askIcon();
|
|
116
|
-
else missing.push(NO_ICON);
|
|
117
|
-
}
|
|
118
|
-
if (missing.length > 0) throw new Error(missing.join('\n'));
|
|
119
|
-
if (appEncryption(state.config) === null) {
|
|
120
|
-
if (isGuided) await askEncryption();
|
|
121
|
-
else console.warn(NO_ENCRYPTION);
|
|
122
|
-
}
|
|
123
|
-
const hermesDsym = await resolveHermesDsym();
|
|
124
|
-
|
|
125
|
-
const facts = { id: appId(state.config), team: appTeam() };
|
|
126
|
-
console.log(`crossbind: publishing ${facts.id} ${appVersion()} to App Store Connect for team ${facts.team}; building the app first.`);
|
|
127
|
-
let archive;
|
|
128
|
-
for (;;) {
|
|
129
|
-
const project = await buildIosApp(viteArgs, runVite);
|
|
130
|
-
console.log('crossbind: archiving the app, which takes a few minutes.');
|
|
131
|
-
archive = await untilDone(isGuided, () => archiveIosApp(project, { facts }));
|
|
132
|
-
const unreasoned = unreasonedImports(archivedBinary(archive), archivedPrivacy(archive));
|
|
133
|
-
if (unreasoned.length === 0) break;
|
|
134
|
-
if (!isGuided) throw unreasonedError(unreasoned);
|
|
135
|
-
await askPrivacy(unreasoned);
|
|
136
|
-
console.log('crossbind: building the app again, with those reasons in its privacy manifest.');
|
|
137
|
-
}
|
|
138
|
-
addHermesDsym(archive, hermesDsym);
|
|
139
|
-
console.log(`crossbind: uploading the app to App Store Connect for team ${facts.team} with ${key ? `the API key ${key.keyId}` : "Xcode's account"}, which signs it.`);
|
|
140
|
-
const build = appBuildNumber();
|
|
141
|
-
await untilDone(isGuided, () => uploadIosArchive(archive, {
|
|
142
|
-
team: facts.team, key, isBuildManaged: build === null, facts,
|
|
143
|
-
}));
|
|
144
|
-
const app = build === null ? `${facts.id} ${appVersion()}, numbered by Xcode after the builds App Store Connect has of it,` : `${facts.id} ${appVersion()} (build ${build})`;
|
|
145
|
-
console.log(`crossbind: ${app} is on App Store Connect. TestFlight hands it out once Apple has processed it, usually within 5 to 30 minutes, and Apple mails when it has.`);
|
|
146
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import attachConanDependencies from './attachConanDependencies.js';
|
|
2
|
-
import { conanStageDir } from '../utils/conanImport.js';
|
|
3
|
-
import { conanManifestsStamp } from '../utils/conanStage.js';
|
|
4
|
-
|
|
5
|
-
const attachedFrom = new WeakMap();
|
|
6
|
-
|
|
7
|
-
// Attaches what a build staged since this config last attached: a process that loaded state before
|
|
8
|
-
// the build, such as a Metro server, would otherwise bind headers without the packages. Says whether
|
|
9
|
-
// it attached again.
|
|
10
|
-
export default function refreshConanDependencies(config) {
|
|
11
|
-
if (Object.keys(config.conanDependencies ?? {}).length === 0) return false;
|
|
12
|
-
const stamp = conanManifestsStamp(conanStageDir(config.paths.cache));
|
|
13
|
-
if (attachedFrom.get(config) === stamp) return false;
|
|
14
|
-
attachedFrom.set(config, stamp);
|
|
15
|
-
attachConanDependencies(config);
|
|
16
|
-
return true;
|
|
17
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { CONAN_NAME } from './conanImport.js';
|
|
2
|
-
import { getContentHash } from './hash.js';
|
|
3
|
-
|
|
4
|
-
// A plain version or a Conan range; a user, channel or recipe revision belongs in conan.lock instead.
|
|
5
|
-
export const CONAN_VERSION = /^[A-Za-z0-9_][A-Za-z0-9_.+-]*$/;
|
|
6
|
-
const VERSION_RANGE = /^\[[^\]\n]+\]$/;
|
|
7
|
-
const OPTION_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
8
|
-
const OPTION_VALUE_TYPES = ['string', 'number', 'boolean'];
|
|
9
|
-
|
|
10
|
-
export function normalizeConanDependencies(raw) {
|
|
11
|
-
if (raw === undefined) return {};
|
|
12
|
-
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
13
|
-
throw new Error('crossbind: "conanDependencies" must be an object such as { zlib: \'1.3.1\' }.');
|
|
14
|
-
}
|
|
15
|
-
return Object.fromEntries(Object.entries(raw).map(([name, spec]) => [name, normalizeSpec(name, spec)]));
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function normalizeSpec(name, spec) {
|
|
19
|
-
if (!CONAN_NAME.test(name)) {
|
|
20
|
-
throw new Error(`crossbind: conanDependencies key '${name}' is not a Conan package name (lower-case letters, digits and _ + . -).`);
|
|
21
|
-
}
|
|
22
|
-
const { version, options = {} } = typeof spec === 'string' ? { version: spec } : (spec ?? {});
|
|
23
|
-
if (typeof version !== 'string' || !(CONAN_VERSION.test(version) || VERSION_RANGE.test(version))) {
|
|
24
|
-
throw new Error(`crossbind: conanDependencies.${name} needs a version such as '1.3.1' or a range such as '[>=1.3 <2]' (got ${JSON.stringify(version)}).`);
|
|
25
|
-
}
|
|
26
|
-
if (options === null || typeof options !== 'object' || Array.isArray(options)) {
|
|
27
|
-
throw new Error(`crossbind: conanDependencies.${name}.options must be an object such as { enable_fts5: true }.`);
|
|
28
|
-
}
|
|
29
|
-
Object.entries(options).forEach(([key, value]) => {
|
|
30
|
-
if (key === 'shared') {
|
|
31
|
-
throw new Error(`crossbind: conanDependencies.${name}.options.shared - crossbind links Conan packages statically into the module and sets shared=False itself.`);
|
|
32
|
-
}
|
|
33
|
-
if (!OPTION_NAME.test(key)) {
|
|
34
|
-
throw new Error(`crossbind: conanDependencies.${name}.options - '${key}' is not an option name.`);
|
|
35
|
-
}
|
|
36
|
-
if (!OPTION_VALUE_TYPES.includes(typeof value)) {
|
|
37
|
-
throw new Error(`crossbind: conanDependencies.${name}.options.${key} must be a string, number or boolean.`);
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
return { version, options: { ...options } };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// What staged packages were installed for; a change to conanDependencies makes them stale.
|
|
44
|
-
export const conanDependenciesKey = (dependencies) => getContentHash(JSON.stringify(dependencies));
|
|
45
|
-
|
|
46
|
-
// What an app's headers and sources see of its Conan packages: the packages are staged after a header
|
|
47
|
-
// may already be bound, and restaged in place when one moves to another version. Null without any.
|
|
48
|
-
export function conanInputsOf(config) {
|
|
49
|
-
if (Object.keys(config.conanDependencies ?? {}).length === 0) return null;
|
|
50
|
-
return {
|
|
51
|
-
dependencies: config.conanDependencies,
|
|
52
|
-
refs: config.allDependencies.filter((d) => d.general.conan).map((d) => d.general.conan.ref),
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export const conanRequires = (dependencies) => Object.entries(dependencies).map(([name, { version }]) => `${name}/${version}`);
|
|
57
|
-
|
|
58
|
-
function optionValue(value) {
|
|
59
|
-
if (value === true) return 'True';
|
|
60
|
-
if (value === false) return 'False';
|
|
61
|
-
return String(value);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export const conanOptionArgs = (dependencies) => Object.entries(dependencies).flatMap(([name, { options }]) => (
|
|
65
|
-
Object.entries(options).flatMap(([key, value]) => ['-o:h', `${name}/*:${key}=${optionValue(value)}`])
|
|
66
|
-
));
|
package/src/utils/conanImport.js
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
|
|
3
|
-
export const CONAN_PREFIX = 'conan:';
|
|
4
|
-
// Conan 2 refuses upper-case package names.
|
|
5
|
-
export const CONAN_NAME = /^[a-z0-9_][a-z0-9_+.-]*$/;
|
|
6
|
-
// No leading dot, so neither '.' nor '..' can climb out of the include directory.
|
|
7
|
-
const HEADER_SEGMENT = /^[A-Za-z0-9_+-][A-Za-z0-9_.+-]*$/;
|
|
8
|
-
|
|
9
|
-
export const conanStageDir = (cacheDir) => `${cacheDir}/conan`;
|
|
10
|
-
// Apart from the stage's own files, so no package name can land on one of them.
|
|
11
|
-
export const conanPackageDir = (stageDir, name) => `${stageDir}/packages/${name}`;
|
|
12
|
-
|
|
13
|
-
export function parseConanImport(source, headerExtensions) {
|
|
14
|
-
const [name, ...headerPath] = source.slice(CONAN_PREFIX.length).split('/');
|
|
15
|
-
if (!CONAN_NAME.test(name)) {
|
|
16
|
-
throw new Error(`crossbind: '${source}' - '${name}' is not a Conan package name; write conan:<package>/<header>, such as conan:zlib/zlib.h.`);
|
|
17
|
-
}
|
|
18
|
-
if (headerPath.length === 0) {
|
|
19
|
-
throw new Error(`crossbind: '${source}' names no header - import one of the package's headers as conan:${name}/<header>.`);
|
|
20
|
-
}
|
|
21
|
-
const bad = headerPath.find((segment) => !HEADER_SEGMENT.test(segment));
|
|
22
|
-
if (bad !== undefined) {
|
|
23
|
-
throw new Error(`crossbind: '${source}' - '${bad}' is not a header path segment.`);
|
|
24
|
-
}
|
|
25
|
-
const header = headerPath.join('/');
|
|
26
|
-
const extension = header.includes('.') ? header.split('.').at(-1) : '';
|
|
27
|
-
if (!headerExtensions.includes(extension)) {
|
|
28
|
-
throw new Error(`crossbind: '${source}' - a conan: import names a header (${headerExtensions.map((e) => `.${e}`).join(', ')}).`);
|
|
29
|
-
}
|
|
30
|
-
return { name, header };
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// The inverse of resolveConanHeader, for the editor types of a staged header.
|
|
34
|
-
export function conanImportOfHeader(file, cacheDir) {
|
|
35
|
-
const prefix = conanPackageDir(conanStageDir(cacheDir), '');
|
|
36
|
-
if (!file.startsWith(prefix)) return null;
|
|
37
|
-
const match = file.slice(prefix.length).match(/^([^/]+)\/dist\/prebuilt\/[^/]+\/include\/(.+)$/);
|
|
38
|
-
// Both end up in the module name of the generated types.
|
|
39
|
-
if (!match || !CONAN_NAME.test(match[1]) || !match[2].split('/').every((segment) => HEADER_SEGMENT.test(segment))) return null;
|
|
40
|
-
return { name: match[1], header: match[2] };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Only declared packages are importable, as with cargo:; a package pulled in by another links but stays hidden.
|
|
44
|
-
export function resolveConanHeader(config, source, target) {
|
|
45
|
-
const { name, header } = parseConanImport(source, config.ext.header);
|
|
46
|
-
if (!Object.hasOwn(config.conanDependencies ?? {}, name)) {
|
|
47
|
-
throw new Error(`crossbind: '${source}' is not declared - add '${name}' to conanDependencies in crossbind.config.`);
|
|
48
|
-
}
|
|
49
|
-
const dependency = config.allDependencies.find((d) => d.general.conan?.name === name);
|
|
50
|
-
if (!dependency) {
|
|
51
|
-
throw new Error(`crossbind: '${source}' - ${name} is declared in conanDependencies but not installed; crossbind installs Conan packages when a build starts.`);
|
|
52
|
-
}
|
|
53
|
-
const tried = [...new Set([target.releasePath, target.path].filter(Boolean))]
|
|
54
|
-
.map((targetPath) => `${dependency.paths.output}/prebuilt/${targetPath}/include/${header}`);
|
|
55
|
-
const found = tried.find((file) => fs.existsSync(file));
|
|
56
|
-
if (found) return found;
|
|
57
|
-
throw new Error(`crossbind: '${source}' - ${name} has no ${header}. Tried:\n${tried.map((file) => ` - ${file}`).join('\n')}`);
|
|
58
|
-
}
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import upath from 'upath';
|
|
3
|
-
import withDirLock from './dirLock.js';
|
|
4
|
-
import logger from './logger.js';
|
|
5
|
-
import loadJson from './loadJson.js';
|
|
6
|
-
import writeIfChanged from './writeIfChanged.js';
|
|
7
|
-
import { getContentHash } from './hash.js';
|
|
8
|
-
import { getDockerImage, imageRoleFor } from './pullDockerImage.js';
|
|
9
|
-
import runConan, {
|
|
10
|
-
clearConanWork, conanRoot, conanRunner, createConanWork, localToolchainIdentity, removeConanWork, toHostPath,
|
|
11
|
-
} from './runConan.js';
|
|
12
|
-
import { hostProfile, settingsUser, BUILD_PROFILE } from './conanProfile.js';
|
|
13
|
-
import { conanRequires, conanOptionArgs, conanDependenciesKey } from './conanDependencies.js';
|
|
14
|
-
import {
|
|
15
|
-
conanPackagesOf, stageConanPackage, writeConanManifest, readConanManifest, isControlCharacter,
|
|
16
|
-
} from './conanStage.js';
|
|
17
|
-
import { conanStageDir } from './conanImport.js';
|
|
18
|
-
|
|
19
|
-
// Bumped when the stage changes shape, so a stage an older crossbind made is made again.
|
|
20
|
-
const CONAN_STAGE_FORMAT = 4;
|
|
21
|
-
// Enough of a failed build's log to reach the error conan ends it with.
|
|
22
|
-
const LOG_TAIL_LINES = 60;
|
|
23
|
-
|
|
24
|
-
const lockFileOf = (config) => upath.join(config.paths.project, 'conan.lock');
|
|
25
|
-
const stampFileOf = (stageDir, target) => upath.join(stageDir, 'stamps', `${target.path}.json`);
|
|
26
|
-
const logFileOf = (stageDir, target) => upath.join(stageDir, 'logs', `${target.path}.log`);
|
|
27
|
-
const printable = (text) => [...text].filter((char) => char === '\n' || char === '\t' || !isControlCharacter(char)).join('');
|
|
28
|
-
|
|
29
|
-
// What a staged target was made from. The toolchain is the image, or the host's compiler and conan where
|
|
30
|
-
// conan runs on the host (RUNNER=LOCAL, and iOS always): the profile asks that compiler for its version,
|
|
31
|
-
// so a new one is a new package id and must restage.
|
|
32
|
-
function stampKey(config, target) {
|
|
33
|
-
const lock = lockFileOf(config);
|
|
34
|
-
return getContentHash(JSON.stringify({
|
|
35
|
-
format: CONAN_STAGE_FORMAT,
|
|
36
|
-
dependencies: config.conanDependencies,
|
|
37
|
-
host: hostProfile(target),
|
|
38
|
-
build: BUILD_PROFILE,
|
|
39
|
-
toolchain: conanRunner(config, target) === 'LOCAL' ? localToolchainIdentity(target) : getDockerImage(imageRoleFor(target)),
|
|
40
|
-
lock: fs.existsSync(lock) ? fs.readFileSync(lock, 'utf8') : null,
|
|
41
|
-
}));
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// The target's manifest names the dependencies it was staged for.
|
|
45
|
-
const isStaged = (config, stageDir, target) => loadJson(stampFileOf(stageDir, target))?.key === stampKey(config, target)
|
|
46
|
-
&& readConanManifest(stageDir, conanDependenciesKey(config.conanDependencies), target.path) !== null;
|
|
47
|
-
|
|
48
|
-
function conanFailure(target, result, logFile) {
|
|
49
|
-
if (result.error && result.error.code !== 'ENOBUFS') {
|
|
50
|
-
return new Error(`crossbind: could not run conan for ${target.path} (${result.error.message}).`);
|
|
51
|
-
}
|
|
52
|
-
let ending = `failed (exit code ${result.status})`;
|
|
53
|
-
if (result.error) ending = 'was stopped: it printed more than crossbind reads';
|
|
54
|
-
else if (result.signal) ending = `was stopped by ${result.signal}`;
|
|
55
|
-
const tail = printable(`${result.stderr ?? ''}`).trim().split('\n').slice(-LOG_TAIL_LINES).join('\n');
|
|
56
|
-
return new Error(`crossbind: conan install for ${target.path} ${ending}. Its whole log is in ${logFile}.\n${tail}`);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// The parsed file, or null. The parse error is left out: it quotes the text, which recipes can write.
|
|
60
|
-
function readJson(file) {
|
|
61
|
-
try {
|
|
62
|
-
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
63
|
-
} catch {
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// The lockfile comes back from where the recipes ran, so only a lockfile goes into the project.
|
|
69
|
-
function lockfileText(file) {
|
|
70
|
-
const lock = readJson(file);
|
|
71
|
-
if (typeof lock?.version !== 'string') {
|
|
72
|
-
throw new Error('crossbind: conan wrote no lockfile crossbind can read, so conan.lock is left as it was.');
|
|
73
|
-
}
|
|
74
|
-
return `${JSON.stringify(lock, null, 4)}\n`;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function installTarget(config, stageDir, target) {
|
|
78
|
-
const dependencies = config.conanDependencies;
|
|
79
|
-
const lock = lockFileOf(config);
|
|
80
|
-
const hasLock = fs.existsSync(lock);
|
|
81
|
-
const stampFile = stampFileOf(stageDir, target);
|
|
82
|
-
const logFile = logFileOf(stageDir, target);
|
|
83
|
-
// A restage that stops halfway must not pass for a finished one.
|
|
84
|
-
fs.rmSync(stampFile, { force: true });
|
|
85
|
-
const work = createConanWork(conanRunner(config, target));
|
|
86
|
-
const label = `conan ${target.path}`;
|
|
87
|
-
logger.startTask(label);
|
|
88
|
-
try {
|
|
89
|
-
fs.writeFileSync(upath.join(work.dir, 'host.profile'), hostProfile(target));
|
|
90
|
-
fs.writeFileSync(upath.join(work.dir, 'build.profile'), BUILD_PROFILE);
|
|
91
|
-
const settings = settingsUser(target);
|
|
92
|
-
if (settings) fs.writeFileSync(upath.join(work.dir, 'home', 'settings_user.yml'), settings);
|
|
93
|
-
if (hasLock) fs.copyFileSync(lock, upath.join(work.dir, 'input.lock'));
|
|
94
|
-
const result = runConan([
|
|
95
|
-
'install',
|
|
96
|
-
...conanRequires(dependencies).flatMap((reference) => ['--requires', reference]),
|
|
97
|
-
...conanOptionArgs(dependencies),
|
|
98
|
-
'-pr:h', work.conanPath('host.profile'),
|
|
99
|
-
'-pr:b', work.conanPath('build.profile'),
|
|
100
|
-
'--output-folder', work.conanPath('output'),
|
|
101
|
-
'--build=missing',
|
|
102
|
-
'--format=json',
|
|
103
|
-
// A file, since a build tool writing to stdout would end up inside the JSON.
|
|
104
|
-
'--out-file', work.conanPath('graph.json'),
|
|
105
|
-
// Unless told which lockfile to use, conan takes any conan.lock in its working directory.
|
|
106
|
-
...(hasLock ? ['--lockfile', work.conanPath('input.lock'), '--lockfile-partial'] : ['--lockfile=']),
|
|
107
|
-
'--lockfile-out', work.conanPath('resolution.lock'),
|
|
108
|
-
], { config, target, work });
|
|
109
|
-
writeIfChanged(logFile, `${result.stderr ?? ''}`);
|
|
110
|
-
if (result.error || result.status !== 0) throw conanFailure(target, result, logFile);
|
|
111
|
-
|
|
112
|
-
const graph = readJson(upath.join(work.dir, 'graph.json'))?.graph;
|
|
113
|
-
if (!graph) throw new Error(`crossbind: conan install wrote no graph crossbind can read. Its log is in ${logFile}.`);
|
|
114
|
-
const packages = conanPackagesOf(graph);
|
|
115
|
-
const distCmake = fs.readFileSync(upath.join(config.paths.cli, 'assets', 'cmake', 'dist.cmake'), 'utf8');
|
|
116
|
-
packages.forEach((pkg) => stageConanPackage(pkg, {
|
|
117
|
-
stageDir, targetPath: target.path, toHost: (reported) => toHostPath(reported, work), store: work.store, distCmake,
|
|
118
|
-
}));
|
|
119
|
-
writeConanManifest(stageDir, target.path, {
|
|
120
|
-
key: conanDependenciesKey(dependencies),
|
|
121
|
-
packages: packages.map(({
|
|
122
|
-
packageFolder, includedirs, libdirs, ...entry
|
|
123
|
-
}) => entry),
|
|
124
|
-
});
|
|
125
|
-
writeIfChanged(lock, lockfileText(upath.join(work.dir, 'resolution.lock')));
|
|
126
|
-
writeIfChanged(stampFile, `${JSON.stringify({ key: stampKey(config, target) })}\n`);
|
|
127
|
-
logger.doneTask(label, packages.map((pkg) => `${pkg.name} ${pkg.version}`).join(', '));
|
|
128
|
-
} finally {
|
|
129
|
-
// Not over the error that ended the run; the next install clears what is left.
|
|
130
|
-
try {
|
|
131
|
-
removeConanWork(work);
|
|
132
|
-
} catch (e) {
|
|
133
|
-
logger.info(`crossbind: could not remove ${work.dir} (${e.message}).`);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Installs the declared Conan packages for every target not staged from the same inputs yet, and
|
|
139
|
-
// says whether it staged any, so the caller attaches the new manifest.
|
|
140
|
-
export default async function installConanPackages(config, targets) {
|
|
141
|
-
if (Object.keys(config.conanDependencies ?? {}).length === 0) return false;
|
|
142
|
-
const stageDir = conanStageDir(config.paths.cache);
|
|
143
|
-
const pending = targets.filter((target) => !isStaged(config, stageDir, target));
|
|
144
|
-
if (pending.length === 0) return false;
|
|
145
|
-
for (const runner of new Set(pending.map((target) => conanRunner(config, target)))) {
|
|
146
|
-
// A conan store takes one writer at a time, and every project on the machine shares it.
|
|
147
|
-
await withDirLock(`${conanRoot(runner)}-install.lock`, async () => {
|
|
148
|
-
clearConanWork(runner);
|
|
149
|
-
pending.filter((target) => conanRunner(config, target) === runner && !isStaged(config, stageDir, target))
|
|
150
|
-
.forEach((target) => installTarget(config, stageDir, target));
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
return true;
|
|
154
|
-
}
|