crossbind 2.0.0-beta.67 → 2.0.0-beta.68

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.
@@ -0,0 +1,77 @@
1
+ import path from 'node:path';
2
+ import { spawnSync } from 'node:child_process';
3
+
4
+ // The C symbols of Apple's required reason API an iOS archive can import, each with its categories; getattrlist and its
5
+ // kin are in two. A selector, as systemUptime or activeInputModes is, is no symbol, so nm finds none of those.
6
+ const SYMBOLS = {
7
+ _stat: ['fileTimestamp'],
8
+ _fstat: ['fileTimestamp'],
9
+ _fstatat: ['fileTimestamp'],
10
+ _lstat: ['fileTimestamp'],
11
+ _getattrlistbulk: ['fileTimestamp'],
12
+ _getattrlist: ['fileTimestamp', 'diskSpace'],
13
+ _fgetattrlist: ['fileTimestamp', 'diskSpace'],
14
+ _getattrlistat: ['fileTimestamp', 'diskSpace'],
15
+ _statfs: ['diskSpace'],
16
+ _statvfs: ['diskSpace'],
17
+ _fstatfs: ['diskSpace'],
18
+ _fstatvfs: ['diskSpace'],
19
+ _mach_absolute_time: ['systemBootTime'],
20
+ '_OBJC_CLASS_$_NSUserDefaults': ['userDefaults'],
21
+ };
22
+ // nm's list of a large library, as GDAL's, runs to megabytes.
23
+ const NM_OUTPUT_LIMIT = 256 * 1024 * 1024;
24
+
25
+ // The categories that the symbols of `nm -u -j` output call, each with those symbols.
26
+ export function reasonCategories(output) {
27
+ const categories = new Map();
28
+ output.split('\n').map((line) => line.trim()).filter((symbol) => Object.hasOwn(SYMBOLS, symbol)).forEach((symbol) => {
29
+ SYMBOLS[symbol].forEach((category) => categories.set(category, (categories.get(category) ?? new Set()).add(symbol)));
30
+ });
31
+ return categories;
32
+ }
33
+
34
+ // The categories an archive or a linked binary imports, as Xcode's nm lists them, whatever nm comes first on the PATH.
35
+ // Its LLVM bitcode reader stays off: the symbol table tells all, and the bitcode Rust's standard library embeds comes
36
+ // from a newer LLVM than an older Xcode reads.
37
+ function importedCategories(file) {
38
+ const result = spawnSync('xcrun', ['nm', '--no-llvm-bc', '-u', '-j', file], { encoding: 'utf8', maxBuffer: NM_OUTPUT_LIMIT });
39
+ if (result.status !== 0) throw new Error(`crossbind: cannot list what ${file} calls: ${result.stderr || result.error?.message}`);
40
+ return reasonCategories(result.stdout);
41
+ }
42
+
43
+ // What the archives an iOS app links call of required reason API with no reason in app.privacy: each library, by the
44
+ // name its archive has, with each such category and its symbols.
45
+ export function unreasonedApis(archives, privacy) {
46
+ const found = new Map();
47
+ archives.forEach((archive) => {
48
+ const library = path.basename(archive).replace(/^\d+-/, '').replace(/^lib/, '').replace(/\.a$/, '');
49
+ importedCategories(archive).forEach((symbols, category) => {
50
+ if (Object.hasOwn(privacy, category)) return;
51
+ const key = `${library}\0${category}`;
52
+ found.set(key, new Set([...(found.get(key) ?? []), ...symbols]));
53
+ });
54
+ });
55
+ return [...found].map(([key, symbols]) => {
56
+ const [library, category] = key.split('\0');
57
+ return { library, category, symbols: [...symbols].sort() };
58
+ }).sort((a, b) => a.library.localeCompare(b.library) || a.category.localeCompare(b.category));
59
+ }
60
+
61
+ // What an iOS app's linked binary calls of required reason API with no reason in app.privacy, each category with its
62
+ // symbols: what the linker kept of its archives, which App Store Connect checks.
63
+ export const unreasonedImports = (binary, privacy) => [...importedCategories(binary)]
64
+ .filter(([category]) => !Object.hasOwn(privacy, category))
65
+ .map(([category, symbols]) => ({ category, symbols: [...symbols].sort() }))
66
+ .sort((a, b) => a.category.localeCompare(b.category));
67
+
68
+ // The warning of what the archives of an iOS app reference of required reason API without its reasons, or none. The
69
+ // linker may leave some of it out of the app, as it left _stat, _lstat and _fstatat out of the conformance app, whose
70
+ // Rust references them; the app keeps what it calls.
71
+ export function unreasonedWarning(found) {
72
+ if (found.length === 0) return null;
73
+ const uses = new Map();
74
+ found.forEach(({ library, category, symbols }) => uses.set(library, [...(uses.get(library) ?? []), `${category} (${symbols.join(', ')})`]));
75
+ const references = [...uses].map(([library, list]) => `${library} references ${list.join(' and ')}`).join('; ');
76
+ return `crossbind: App Store Connect refuses an iOS app that calls required reason API without its reasons (ITMS-91053), and the archives this app links reference some with no reason in app.privacy: ${references}. The linker may leave some out. Give app.privacy Apple's reasons for what the app calls, as { fileTimestamp: ['C617.1'] }; Apple's "Describing use of required reason API" says what each covers.`;
77
+ }
@@ -0,0 +1,23 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ let folder = null;
6
+
7
+ // Removes the folder of the secrets written, as crossbind does when it exits.
8
+ export function removeSecretFiles() {
9
+ if (folder) fs.rmSync(folder, { recursive: true, force: true });
10
+ folder = null;
11
+ }
12
+
13
+ // A secret the environment holds as its text, as CI's secrets come, written to a file of a folder only this user opens,
14
+ // as the tools that take it read files. The folder goes when crossbind exits.
15
+ export function secretFile(name, data) {
16
+ if (!folder) {
17
+ folder = fs.mkdtempSync(path.join(os.tmpdir(), 'crossbind-secrets-'));
18
+ process.once('exit', removeSecretFiles);
19
+ }
20
+ const file = path.join(folder, name);
21
+ fs.writeFileSync(file, data, { mode: 0o600 });
22
+ return file;
23
+ }
@@ -1,6 +1,7 @@
1
1
  import os from 'node:os';
2
2
  import loadJson from './loadJson.js';
3
3
  import systemKeys from './systemKeys.js';
4
+ import writeJson from './writeJson.js';
4
5
 
5
6
  export const systemConfigPath = () => `${os.homedir()}/.crossbind.json`;
6
7
 
@@ -11,3 +12,8 @@ export default function loadSystemConfig() {
11
12
  .map(([key, value]) => [key, value.default]);
12
13
  return { ...system, ...Object.fromEntries(defaults) };
13
14
  }
15
+
16
+ // Sets keys of ~/.crossbind.json, as `crossbind config set` does, keeping the others.
17
+ export function updateSystemConfig(values) {
18
+ writeJson(systemConfigPath(), { ...loadSystemConfig(), ...values });
19
+ }
@@ -3,6 +3,22 @@ const systemKeys = {
3
3
  description: 'The unique identifier of the development team used for code signing and app distribution in Xcode.',
4
4
  default: '',
5
5
  },
6
+ ANDROID_KEYSTORE: {
7
+ description: "This machine's upload key, which signs an Android app's release and its bundle for Google Play; crossbind app upload-key sets it. The CROSSBIND_ANDROID_KEYSTORE environment variable overrides it.",
8
+ default: '',
9
+ },
10
+ ANDROID_KEY_ALIAS: {
11
+ description: 'The alias of the upload key in ANDROID_KEYSTORE. The CROSSBIND_ANDROID_KEY_ALIAS environment variable overrides it.',
12
+ default: '',
13
+ },
14
+ GOOGLE_PLAY_SERVICE_ACCOUNT: {
15
+ description: "The e-mail of the service account crossbind app publish uploads to Google Play as, through this machine's gcloud login, so no key of it exists. The CROSSBIND_GOOGLE_PLAY_SERVICE_ACCOUNT environment variable overrides it.",
16
+ default: '',
17
+ },
18
+ GOOGLE_PLAY_KEY: {
19
+ description: 'The JSON key of the service account crossbind app publish uploads to Google Play as, where gcloud is not, as in CI. The CROSSBIND_GOOGLE_PLAY_KEY environment variable overrides it.',
20
+ default: '',
21
+ },
6
22
  RUNNER: {
7
23
  description: 'Where toolchain steps run: DOCKER_RUN, DOCKER_EXEC, LOCAL, or REMOTE on the runners REMOTE_URL and REMOTE_URL_<IMAGE> name; a machine that names none builds on crossbind cloud once signed in with crossbind login. The CROSSBIND_RUNNER environment variable overrides it.',
8
24
  options: ['DOCKER_RUN', 'DOCKER_EXEC', 'LOCAL', 'REMOTE'],