@atom-js-org/cli 0.4.2-alpha.0 → 0.4.4-alpha.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atom-js-org/cli",
3
- "version": "0.4.2-alpha.0",
3
+ "version": "0.4.4-alpha.0",
4
4
  "description": "CLI for running and building AtomJS desktop applications.",
5
5
  "bin": {
6
6
  "atom": "bin/atom.cjs"
package/src/build.cjs CHANGED
@@ -70,6 +70,7 @@ async function localBuild(project, target, options = {}) {
70
70
  appDir,
71
71
  target,
72
72
  productName,
73
+ appId: project.config.appId,
73
74
  payloadPath
74
75
  });
75
76
 
@@ -106,7 +107,7 @@ async function localBuild(project, target, options = {}) {
106
107
  }
107
108
 
108
109
  const manifest = {
109
- atomjsVersion: '0.4.2-alpha.0',
110
+ atomjsVersion: '0.4.4-alpha.0',
110
111
  target,
111
112
  productName,
112
113
  appId: project.config.appId,
@@ -287,7 +288,7 @@ async function createApplicationPayload(appDir, outputPath) {
287
288
  await fs.promises.writeFile(outputPath, compressed);
288
289
  }
289
290
 
290
- async function createSeaLauncher({ executablePath, appDir, target, productName, payloadPath = null }) {
291
+ async function createSeaLauncher({ executablePath, appDir, target, productName, appId, payloadPath = null }) {
291
292
  const work = path.join(path.dirname(executablePath), '.sea-' + crypto.randomBytes(5).toString('hex'));
292
293
  await fse.ensureDir(work);
293
294
  const launcherPath = path.join(work, 'launcher.cjs');
@@ -295,7 +296,7 @@ async function createSeaLauncher({ executablePath, appDir, target, productName,
295
296
  const configPath = path.join(work, 'sea-config.json');
296
297
 
297
298
  if (!payloadPath) throw new Error('AtomJS SEA payload is required.');
298
- const launcher = createEmbeddedLauncherSource(productName);
299
+ const launcher = createEmbeddedLauncherSource(productName, appId);
299
300
  await fs.promises.writeFile(launcherPath, launcher, 'utf8');
300
301
 
301
302
  const seaConfig = {
@@ -314,8 +315,8 @@ async function createSeaLauncher({ executablePath, appDir, target, productName,
314
315
  if (target === 'windows') {
315
316
  await prepareWindowsExecutableForInjection(executablePath);
316
317
  }
317
- if (target === 'macos' && commandExists('codesign', ['--version'])) {
318
- spawnSync('codesign', ['--remove-signature', executablePath], { stdio: 'ignore' });
318
+ if (target === 'macos' && hasMacCodeSigningTool()) {
319
+ spawnSync('/usr/bin/codesign', ['--remove-signature', executablePath], { stdio: 'ignore' });
319
320
  }
320
321
 
321
322
  await inject(executablePath, 'NODE_SEA_BLOB', await fs.promises.readFile(blobPath), {
@@ -328,8 +329,8 @@ async function createSeaLauncher({ executablePath, appDir, target, productName,
328
329
  } else {
329
330
  await fs.promises.chmod(executablePath, 0o755);
330
331
  }
331
- if (target === 'macos' && commandExists('codesign', ['--version'])) {
332
- await run('codesign', ['--force', '--sign', '-', executablePath]);
332
+ if (target === 'macos' && hasMacCodeSigningTool()) {
333
+ await run('/usr/bin/codesign', ['--force', '--sign', '-', executablePath]);
333
334
  }
334
335
  await fse.remove(work);
335
336
  }
@@ -396,7 +397,7 @@ function readPortableExecutableLayout(image) {
396
397
  };
397
398
  }
398
399
 
399
- function createEmbeddedLauncherSource(productName) {
400
+ function createEmbeddedLauncherSource(productName, appId) {
400
401
  return `
401
402
  'use strict';
402
403
  const path = require('node:path');
@@ -409,6 +410,7 @@ const { pathToFileURL } = require('node:url');
409
410
  const { getAsset } = require('node:sea');
410
411
 
411
412
  const productName = ${JSON.stringify(productName)};
413
+ const appId = ${JSON.stringify(appId || 'com.atomjs.app')};
412
414
  const payload = Buffer.from(getAsset('atom-app'));
413
415
  const payloadHash = crypto.createHash('sha256').update(payload).digest('hex').slice(0, 24);
414
416
  const dataRoot = process.platform === 'darwin'
@@ -448,10 +450,16 @@ if (!fs.existsSync(packagePath)) throw new Error('AtomJS could not materialize t
448
450
  const executableDir = path.dirname(process.execPath);
449
451
  process.chdir(appDir);
450
452
  process.env.ATOM_PROJECT_ROOT = appDir;
453
+ process.env.ATOM_APP_NAME = productName;
454
+ process.env.ATOM_APP_ID = appId;
451
455
  process.env.ATOM_BUILD = '1';
452
456
  process.env.ATOM_EMBEDDED_RUNTIME = '1';
453
457
  process.env.ATOM_WINDOW_HOST_ENTRY = path.join(appDir, 'vendor', 'atomjs', 'src', 'runtime', 'window-host.mjs');
454
458
  process.env.ATOM_MACOS_HOST_EXECUTABLE = path.join(executableDir, 'AtomJSWindowHost');
459
+ const bundledMacIcon = path.join(executableDir, '..', 'Resources', 'AppIcon.icns');
460
+ if (process.platform === 'darwin' && fs.existsSync(bundledMacIcon)) {
461
+ process.env.ATOM_APP_ICON = bundledMacIcon;
462
+ }
455
463
  const hostModeIndex = process.argv.indexOf('--atomjs-window-host');
456
464
  if (hostModeIndex !== -1) {
457
465
  const hostEntry = process.env.ATOM_WINDOW_HOST_ENTRY;
@@ -558,50 +566,105 @@ function resolveNsisExecutable() {
558
566
  return candidates.find((candidate) => fs.existsSync(candidate)) || null;
559
567
  }
560
568
 
569
+ function hasMacCodeSigningTool() {
570
+ return process.platform === 'darwin' && fs.existsSync('/usr/bin/codesign');
571
+ }
572
+
573
+ async function removeMacMetadataFiles(root) {
574
+ const removed = [];
575
+ if (!root || !fs.existsSync(root)) return removed;
576
+
577
+ async function visit(directory) {
578
+ const entries = await fs.promises.readdir(directory, { withFileTypes: true });
579
+ for (const entry of entries) {
580
+ const absolute = path.join(directory, entry.name);
581
+ const isMetadata = entry.name === '.DS_Store'
582
+ || entry.name === '__MACOSX'
583
+ || entry.name.startsWith('._');
584
+
585
+ if (isMetadata) {
586
+ await fs.promises.rm(absolute, { recursive: true, force: true });
587
+ removed.push(absolute);
588
+ continue;
589
+ }
590
+
591
+ if (entry.isDirectory()) await visit(absolute);
592
+ }
593
+ }
594
+
595
+ await visit(root);
596
+ return removed;
597
+ }
598
+
599
+ async function sanitizeMacBundle(appBundle) {
600
+ await removeMacMetadataFiles(appBundle);
601
+
602
+ if (process.platform === 'darwin' && fs.existsSync('/usr/bin/xattr')) {
603
+ const result = spawnSync('/usr/bin/xattr', ['-cr', appBundle], {
604
+ encoding: 'utf8',
605
+ stdio: ['ignore', 'pipe', 'pipe']
606
+ });
607
+ if (result.error) {
608
+ console.warn(`AtomJS could not clear macOS extended attributes: ${result.error.message}`);
609
+ } else if (result.status !== 0) {
610
+ const detail = String(result.stderr || result.stdout || '').trim();
611
+ if (detail) console.warn(`AtomJS could not clear every macOS extended attribute: ${detail}`);
612
+ }
613
+ }
614
+
615
+ // Filesystems such as exFAT can materialize extended attributes as AppleDouble
616
+ // sidecars while xattr is running, so remove metadata once more afterwards.
617
+ await removeMacMetadataFiles(appBundle);
618
+ }
619
+
561
620
  async function packageMacOS({ project, buildRoot, unpacked, executableName, productName, hostSource }) {
562
621
  const outputs = [];
563
- const appBundle = path.join(buildRoot, `${productName}.app`);
622
+ const finalAppBundle = path.join(buildRoot, `${productName}.app`);
623
+ const stageBase = await resolveShortStageBase();
624
+ const bundleStageRoot = await fs.promises.mkdtemp(path.join(stageBase, 'macos-app-'));
625
+ const appBundle = path.join(bundleStageRoot, `${productName}.app`);
564
626
  const contents = path.join(appBundle, 'Contents');
565
627
  const macosDir = path.join(contents, 'MacOS');
566
628
  const resources = path.join(contents, 'Resources');
567
629
  const mainExecutable = path.join(macosDir, productName);
568
630
  const nativeHost = path.join(macosDir, 'AtomJSWindowHost');
569
631
 
570
- await fse.ensureDir(macosDir);
571
- await fse.ensureDir(resources);
572
- await fse.copy(path.join(unpacked, executableName), mainExecutable);
573
- await fse.copy(path.join(unpacked, 'ATOMJS-CREDIT.txt'), path.join(resources, 'ATOMJS-CREDIT.txt'));
574
- await fs.promises.chmod(mainExecutable, 0o755);
575
-
576
- if (!fs.existsSync(hostSource)) {
577
- throw new Error(`AtomJS macOS native host source was not found: ${hostSource}`);
578
- }
579
- if (!commandExists('/usr/bin/xcrun', ['--version'])) {
580
- throw new Error('macOS builds require the Xcode Command Line Tools. Run `xcode-select --install`.');
581
- }
582
-
583
- await run('/usr/bin/xcrun', [
584
- 'clang',
585
- '-fobjc-arc',
586
- '-fmodules',
587
- '-mmacosx-version-min=12.0',
588
- '-framework', 'Cocoa',
589
- '-framework', 'WebKit',
590
- hostSource,
591
- '-o', nativeHost
592
- ]);
593
- await fs.promises.chmod(nativeHost, 0o755);
632
+ try {
633
+ await fse.ensureDir(macosDir);
634
+ await fse.ensureDir(resources);
635
+ await fse.copy(path.join(unpacked, executableName), mainExecutable);
636
+ await fse.copy(path.join(unpacked, 'ATOMJS-CREDIT.txt'), path.join(resources, 'ATOMJS-CREDIT.txt'));
637
+ await fs.promises.chmod(mainExecutable, 0o755);
638
+
639
+ if (!fs.existsSync(hostSource)) {
640
+ throw new Error(`AtomJS macOS native host source was not found: ${hostSource}`);
641
+ }
642
+ if (!commandExists('/usr/bin/xcrun', ['--version'])) {
643
+ throw new Error('macOS builds require the Xcode Command Line Tools. Run `xcode-select --install`.');
644
+ }
594
645
 
595
- let iconEntry = '';
596
- const iconSource = project.config.icon ? path.resolve(project.root, project.config.icon) : null;
597
- if (iconSource && fs.existsSync(iconSource) && path.extname(iconSource).toLowerCase() === '.icns') {
598
- await fse.copy(iconSource, path.join(resources, 'AppIcon.icns'));
599
- iconEntry = '<key>CFBundleIconFile</key><string>AppIcon</string>';
600
- }
646
+ await run('/usr/bin/xcrun', [
647
+ 'clang',
648
+ '-fobjc-arc',
649
+ '-fmodules',
650
+ '-mmacosx-version-min=12.0',
651
+ '-framework', 'Cocoa',
652
+ '-framework', 'WebKit',
653
+ hostSource,
654
+ '-o', nativeHost
655
+ ]);
656
+ await fs.promises.chmod(nativeHost, 0o755);
657
+
658
+ let iconEntry = '';
659
+ const iconSource = project.config.icon ? path.resolve(project.root, project.config.icon) : null;
660
+ if (iconSource && fs.existsSync(iconSource) && path.extname(iconSource).toLowerCase() === '.icns') {
661
+ await fse.copy(iconSource, path.join(resources, 'AppIcon.icns'));
662
+ iconEntry = '<key>CFBundleIconFile</key><string>AppIcon</string>';
663
+ }
601
664
 
602
- const version = String(project.packageJson.version || '0.0.0');
603
- const bundleVersion = (version.match(/\d+/g) || ['1']).slice(0, 3).join('.');
604
- const plist = `<?xml version="1.0" encoding="UTF-8"?>
665
+ const version = String(project.packageJson.version || '0.0.0');
666
+ const bundleVersion = (version.match(/\d+/g) || ['1']).slice(0, 3).join('.');
667
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
605
668
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
606
669
  <plist version="1.0"><dict>
607
670
  <key>CFBundleExecutable</key><string>${xml(productName)}</string>
@@ -615,35 +678,64 @@ async function packageMacOS({ project, buildRoot, unpacked, executableName, prod
615
678
  <key>NSHighResolutionCapable</key><true/>
616
679
  ${iconEntry}
617
680
  </dict></plist>`;
618
- const plistPath = path.join(contents, 'Info.plist');
619
- await fs.promises.writeFile(plistPath, plist, 'utf8');
681
+ const plistPath = path.join(contents, 'Info.plist');
682
+ await fs.promises.writeFile(plistPath, plist, 'utf8');
620
683
 
621
- if (commandExists('/usr/bin/plutil', ['-help'])) {
622
- await run('/usr/bin/plutil', ['-lint', plistPath]);
623
- }
684
+ if (commandExists('/usr/bin/plutil', ['-help'])) {
685
+ await run('/usr/bin/plutil', ['-lint', plistPath]);
686
+ }
624
687
 
625
- if (commandExists('/usr/bin/codesign', ['--version'])) {
626
- await run('/usr/bin/codesign', ['--force', '--sign', '-', nativeHost]);
627
- await run('/usr/bin/codesign', ['--force', '--sign', '-', mainExecutable]);
628
- await run('/usr/bin/codesign', ['--force', '--deep', '--sign', '-', appBundle]);
629
- await run('/usr/bin/codesign', ['--verify', '--deep', '--strict', appBundle]);
630
- }
688
+ await sanitizeMacBundle(appBundle);
631
689
 
632
- const zipPath = path.join(buildRoot, `${productName}-macos.zip`);
633
- if (commandExists('ditto', ['-h'])) {
634
- await run('ditto', ['-c', '-k', '--sequesterRsrc', '--keepParent', appBundle, zipPath]);
635
- } else {
636
- await archiveDirectory(appBundle, zipPath, 'zip', `${productName}.app`);
637
- }
638
- outputs.push(zipPath);
690
+ if (hasMacCodeSigningTool()) {
691
+ const signingEnvironment = { ...process.env, COPYFILE_DISABLE: '1' };
692
+ await run('/usr/bin/codesign', ['--force', '--sign', '-', nativeHost], { env: signingEnvironment });
693
+ await run('/usr/bin/codesign', ['--force', '--sign', '-', mainExecutable], { env: signingEnvironment });
639
694
 
640
- if (commandExists('hdiutil', ['help'])) {
641
- const dmgPath = path.join(buildRoot, `${productName}.dmg`);
642
- await run('hdiutil', ['create', '-volname', productName, '-srcfolder', appBundle, '-ov', '-format', 'UDZO', dmgPath]);
643
- outputs.push(dmgPath);
644
- }
695
+ // Signing nested Mach-O files on non-APFS volumes can create fresh `._*`
696
+ // sidecars. They must not be present when the outer bundle is sealed.
697
+ await sanitizeMacBundle(appBundle);
698
+ await run('/usr/bin/codesign', ['--force', '--deep', '--sign', '-', appBundle], { env: signingEnvironment });
699
+ await run('/usr/bin/codesign', ['--verify', '--deep', '--strict', '--verbose=2', appBundle], { env: signingEnvironment });
700
+ }
645
701
 
646
- return { outputs, appBundle };
702
+ // Assemble and sign on the local temporary filesystem, then copy the sealed
703
+ // bundle to the project output. This avoids AppleDouble metadata generated by
704
+ // external/exFAT project drives such as /Volumes/... during codesign.
705
+ await fse.remove(finalAppBundle);
706
+ await fse.copy(appBundle, finalAppBundle);
707
+ await fs.promises.chmod(path.join(finalAppBundle, 'Contents', 'MacOS', productName), 0o755);
708
+ await fs.promises.chmod(path.join(finalAppBundle, 'Contents', 'MacOS', 'AtomJSWindowHost'), 0o755);
709
+ await sanitizeMacBundle(finalAppBundle);
710
+
711
+ if (hasMacCodeSigningTool()) {
712
+ await run('/usr/bin/codesign', ['--verify', '--deep', '--strict', '--verbose=2', finalAppBundle], {
713
+ env: { ...process.env, COPYFILE_DISABLE: '1' }
714
+ });
715
+ }
716
+
717
+ const zipPath = path.join(buildRoot, `${productName}-macos.zip`);
718
+ if (commandExists('ditto', ['-h'])) {
719
+ await run('ditto', ['--norsrc', '-c', '-k', '--keepParent', finalAppBundle, zipPath], {
720
+ env: { ...process.env, COPYFILE_DISABLE: '1' }
721
+ });
722
+ } else {
723
+ await archiveDirectory(finalAppBundle, zipPath, 'zip', `${productName}.app`);
724
+ }
725
+ outputs.push(zipPath);
726
+
727
+ if (commandExists('hdiutil', ['help'])) {
728
+ const dmgPath = path.join(buildRoot, `${productName}.dmg`);
729
+ await run('hdiutil', ['create', '-volname', productName, '-srcfolder', finalAppBundle, '-ov', '-format', 'UDZO', dmgPath], {
730
+ env: { ...process.env, COPYFILE_DISABLE: '1' }
731
+ });
732
+ outputs.push(dmgPath);
733
+ }
734
+
735
+ return { outputs, appBundle: finalAppBundle };
736
+ } finally {
737
+ await fse.remove(bundleStageRoot);
738
+ }
647
739
  }
648
740
 
649
741
  async function packageLinux({ project, buildRoot, unpacked, executableName, productName }) {
@@ -796,4 +888,4 @@ function xml(value) {
796
888
  return String(value).replace(/[<>&'\"]/g, (char) => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;', "'": '&apos;', '"': '&quot;' })[char]);
797
889
  }
798
890
 
799
- module.exports = { buildCommand, localBuild, createApplicationPayload, readPortableExecutableLayout };
891
+ module.exports = { buildCommand, localBuild, createApplicationPayload, readPortableExecutableLayout, removeMacMetadataFiles };
package/src/doctor.cjs CHANGED
@@ -19,7 +19,7 @@ async function doctorCommand(options = {}) {
19
19
  rows.push(check('Xcode Command Line Tools', commandExists('/usr/bin/xcrun', ['--version']), 'required to compile the native Cocoa host'));
20
20
  rows.push(check('Clang', commandExists('/usr/bin/xcrun', ['clang', '--version'])));
21
21
  rows.push(check('WebKit framework', fs.existsSync('/System/Library/Frameworks/WebKit.framework')));
22
- rows.push(check('codesign', commandExists('/usr/bin/codesign', ['--version'])));
22
+ rows.push(check('codesign', fs.existsSync('/usr/bin/codesign'), '/usr/bin/codesign'));
23
23
  rows.push(check('hdiutil', commandExists('hdiutil', ['help'])));
24
24
  } else {
25
25
  rows.push(check('pkg-config', commandExists('pkg-config', ['--version'])));
package/src/init.cjs CHANGED
@@ -30,14 +30,14 @@ async function initCommand(directory, options = {}) {
30
30
  start: 'atom run build'
31
31
  },
32
32
  dependencies: {
33
- '@atom-js-org/runtime': '0.4.2-alpha.0',
34
- electron: 'npm:@atom-js-org/electron@0.4.2-alpha.0'
33
+ '@atom-js-org/runtime': '0.4.4-alpha.0',
34
+ electron: 'npm:@atom-js-org/electron@0.4.4-alpha.0'
35
35
  },
36
36
  optionalDependencies: {
37
37
  'webview-nodejs': '0.5.0'
38
38
  },
39
39
  devDependencies: {
40
- '@atom-js-org/cli': '0.4.2-alpha.0'
40
+ '@atom-js-org/cli': '0.4.4-alpha.0'
41
41
  },
42
42
  overrides: {
43
43
  tar: '7.5.20'
package/src/run.cjs CHANGED
@@ -23,11 +23,17 @@ async function runDev(options) {
23
23
 
24
24
  await ensureElectronCompatibility(project.root);
25
25
 
26
+ const iconPath = project.config.icon
27
+ ? path.resolve(project.root, project.config.icon)
28
+ : null;
26
29
  const child = spawn(process.execPath, [mainPath], {
27
30
  cwd: project.root,
28
31
  env: {
29
32
  ...process.env,
30
33
  ATOM_PROJECT_ROOT: project.root,
34
+ ATOM_APP_NAME: project.config.productName,
35
+ ATOM_APP_ID: project.config.appId,
36
+ ...(iconPath && fs.existsSync(iconPath) ? { ATOM_APP_ICON: iconPath } : {}),
31
37
  ATOM_DEV: '1'
32
38
  },
33
39
  stdio: 'inherit'