@atom-js-org/cli 0.4.5-alpha.0 → 0.5.0-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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # AtomJS CLI
2
2
 
3
- Run and build AtomJS applications.
3
+ Run and package AtomJS applications:
4
4
 
5
5
  ```bash
6
6
  atom run dev
@@ -11,4 +11,6 @@ atom build linux
11
11
  atom build all
12
12
  ```
13
13
 
14
+ `atom.config.json` controls artifact names, Windows EXE/NSIS branding, macOS app/DMG metadata and Linux AppImage/DEB/RPM output. New projects include default PNG and ICO assets that can be replaced directly.
15
+
14
16
  Project: https://github.com/Atom-js-org/atom
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atom-js-org/cli",
3
- "version": "0.4.5-alpha.0",
3
+ "version": "0.5.0-alpha.0",
4
4
  "description": "CLI for running and building AtomJS desktop applications.",
5
5
  "bin": {
6
6
  "atom": "bin/atom.cjs"
@@ -9,6 +9,7 @@
9
9
  "files": [
10
10
  "bin/",
11
11
  "src/",
12
+ "assets/",
12
13
  "README.md",
13
14
  "LICENSE"
14
15
  ],
@@ -19,7 +20,8 @@
19
20
  "archiver": "7.0.1",
20
21
  "commander": "^15.0.0",
21
22
  "fs-extra": "^11.3.6",
22
- "postject": "1.0.0-alpha.6"
23
+ "postject": "1.0.0-alpha.6",
24
+ "rcedit": "4.0.1"
23
25
  },
24
26
  "repository": {
25
27
  "type": "git",
package/src/build.cjs CHANGED
@@ -71,6 +71,7 @@ async function localBuild(project, target, options = {}) {
71
71
  target,
72
72
  productName,
73
73
  appId: project.config.appId,
74
+ project,
74
75
  payloadPath
75
76
  });
76
77
 
@@ -107,7 +108,7 @@ async function localBuild(project, target, options = {}) {
107
108
  }
108
109
 
109
110
  const manifest = {
110
- atomjsVersion: '0.4.5-alpha.0',
111
+ atomjsVersion: '0.5.0-alpha.0',
111
112
  target,
112
113
  productName,
113
114
  appId: project.config.appId,
@@ -288,7 +289,7 @@ async function createApplicationPayload(appDir, outputPath) {
288
289
  await fs.promises.writeFile(outputPath, compressed);
289
290
  }
290
291
 
291
- async function createSeaLauncher({ executablePath, appDir, target, productName, appId, payloadPath = null }) {
292
+ async function createSeaLauncher({ executablePath, appDir, target, productName, appId, project = null, payloadPath = null }) {
292
293
  const work = path.join(path.dirname(executablePath), '.sea-' + crypto.randomBytes(5).toString('hex'));
293
294
  await fse.ensureDir(work);
294
295
  const launcherPath = path.join(work, 'launcher.cjs');
@@ -313,6 +314,8 @@ async function createSeaLauncher({ executablePath, appDir, target, productName,
313
314
  await fs.promises.copyFile(process.execPath, executablePath);
314
315
 
315
316
  if (target === 'windows') {
317
+ if (!project) throw new Error('AtomJS requires project metadata to customize a Windows executable.');
318
+ await customizeWindowsExecutable({ project, executablePath, productName });
316
319
  await prepareWindowsExecutableForInjection(executablePath);
317
320
  }
318
321
  if (target === 'macos' && hasMacCodeSigningTool()) {
@@ -478,66 +481,326 @@ load(mainPath);
478
481
  `;
479
482
  }
480
483
 
484
+
485
+ function normalizeWindowsVersion(value) {
486
+ const parts = String(value || '0.0.0').match(/\d+/g) || ['0'];
487
+ return [...parts.slice(0, 4), '0', '0', '0', '0'].slice(0, 4).join('.');
488
+ }
489
+
490
+ function resolveProjectAsset(project, configured, expectedExtension = null) {
491
+ if (!configured) return null;
492
+ const resolved = path.resolve(project.root, configured);
493
+ if (!fs.existsSync(resolved)) {
494
+ throw new Error(`AtomJS build asset was not found: ${resolved}`);
495
+ }
496
+ if (expectedExtension && path.extname(resolved).toLowerCase() !== expectedExtension.toLowerCase()) {
497
+ throw new Error(`AtomJS expected a ${expectedExtension} asset: ${resolved}`);
498
+ }
499
+ return resolved;
500
+ }
501
+
502
+ function platformArchitecture() {
503
+ if (process.arch === 'x64') return 'x64';
504
+ if (process.arch === 'arm64') return 'arm64';
505
+ if (process.arch === 'ia32') return 'ia32';
506
+ return process.arch;
507
+ }
508
+
509
+ function renderArtifactBase(project, target, templateOverride = null) {
510
+ const template = templateOverride || project.config.build.artifactName;
511
+ const variables = {
512
+ productName: project.config.productName,
513
+ version: String(project.packageJson.version || '0.0.0'),
514
+ target,
515
+ arch: platformArchitecture(),
516
+ appId: project.config.appId
517
+ };
518
+ const rendered = String(template || '${productName}-${version}-${target}-${arch}')
519
+ .replace(/\$\{(productName|version|target|arch|appId)\}/g, (_, key) => variables[key]);
520
+ return sanitizeFilename(rendered);
521
+ }
522
+
523
+ function packageAuthor(project) {
524
+ const author = project.packageJson.author;
525
+ if (typeof author === 'string' && author.trim()) return author.trim();
526
+ if (author && typeof author === 'object') {
527
+ const name = String(author.name || '').trim();
528
+ const email = String(author.email || '').trim();
529
+ if (name && email) return `${name} <${email}>`;
530
+ if (name) return name;
531
+ }
532
+ return 'AtomJS application';
533
+ }
534
+
535
+ function normalizeDebVersion(value) {
536
+ const normalized = String(value || '0.0.0')
537
+ .replace(/[^0-9A-Za-z.+:~\-]/g, '-')
538
+ .replace(/^-+/, '');
539
+ return normalized || '0.0.0';
540
+ }
541
+
542
+ function debArchitecture() {
543
+ if (process.arch === 'x64') return 'amd64';
544
+ if (process.arch === 'arm64') return 'arm64';
545
+ if (process.arch === 'ia32') return 'i386';
546
+ return process.arch;
547
+ }
548
+
549
+ function normalizeRpmVersion(value) {
550
+ const normalized = String(value || '0.0.0')
551
+ .replace(/[^0-9A-Za-z.+~]/g, '.')
552
+ .replace(/^\.+|\.+$/g, '');
553
+ return normalized || '0.0.0';
554
+ }
555
+
556
+ function rpmArchitecture() {
557
+ if (process.arch === 'x64') return 'x86_64';
558
+ if (process.arch === 'arm64') return 'aarch64';
559
+ if (process.arch === 'ia32') return 'i686';
560
+ return process.arch;
561
+ }
562
+
563
+ function rpmText(value) {
564
+ return String(value || '').replace(/%/g, '%%').replace(/\r?\n/g, ' ').trim();
565
+ }
566
+
567
+ async function writeArArchive(outputPath, members) {
568
+ const chunks = [Buffer.from('!<arch>\n', 'ascii')];
569
+ for (const member of members) {
570
+ const data = Buffer.isBuffer(member.data) ? member.data : Buffer.from(member.data);
571
+ const name = `${String(member.name).slice(0, 15)}/`.padEnd(16, ' ');
572
+ const timestamp = String(Math.floor(Date.now() / 1000)).padEnd(12, ' ');
573
+ const owner = '0'.padEnd(6, ' ');
574
+ const group = '0'.padEnd(6, ' ');
575
+ const mode = '100644'.padEnd(8, ' ');
576
+ const size = String(data.length).padEnd(10, ' ');
577
+ const header = Buffer.from(`${name}${timestamp}${owner}${group}${mode}${size}\x60\n`, 'ascii');
578
+ chunks.push(header, data);
579
+ if (data.length % 2 !== 0) chunks.push(Buffer.from('\n'));
580
+ }
581
+ await fs.promises.writeFile(outputPath, Buffer.concat(chunks));
582
+ }
583
+
584
+ async function prepareMacIcon(project, resources, workRoot) {
585
+ const configured = project.config.build.macos.icon;
586
+ if (!configured) return null;
587
+ const source = resolveProjectAsset(project, configured);
588
+ const extension = path.extname(source).toLowerCase();
589
+ const destination = path.join(resources, 'AppIcon.icns');
590
+
591
+ if (extension === '.icns') {
592
+ await fse.copy(source, destination);
593
+ return destination;
594
+ }
595
+
596
+ if (extension !== '.png') {
597
+ throw new Error(`AtomJS macOS icons must be .icns or .png: ${source}`);
598
+ }
599
+ if (!commandExists('/usr/bin/sips', ['--help']) || !commandExists('/usr/bin/iconutil', ['--help'])) {
600
+ throw new Error('Converting a PNG macOS icon requires the system sips and iconutil tools.');
601
+ }
602
+
603
+ const iconset = path.join(workRoot, 'AppIcon.iconset');
604
+ await fse.remove(iconset);
605
+ await fse.ensureDir(iconset);
606
+ const entries = [
607
+ [16, 'icon_16x16.png'],
608
+ [32, 'icon_16x16@2x.png'],
609
+ [32, 'icon_32x32.png'],
610
+ [64, 'icon_32x32@2x.png'],
611
+ [128, 'icon_128x128.png'],
612
+ [256, 'icon_128x128@2x.png'],
613
+ [256, 'icon_256x256.png'],
614
+ [512, 'icon_256x256@2x.png'],
615
+ [512, 'icon_512x512.png'],
616
+ [1024, 'icon_512x512@2x.png']
617
+ ];
618
+ for (const [size, filename] of entries) {
619
+ await run('/usr/bin/sips', ['-z', String(size), String(size), source, '--out', path.join(iconset, filename)]);
620
+ }
621
+ await run('/usr/bin/iconutil', ['-c', 'icns', iconset, '-o', destination]);
622
+ return destination;
623
+ }
624
+
625
+ function codesignArguments(project, targetPath, deep = false) {
626
+ const config = project.config.build.macos;
627
+ const args = ['--force'];
628
+ if (deep) args.push('--deep');
629
+ if (config.hardenedRuntime) args.push('--options', 'runtime');
630
+ const entitlements = config.entitlements ? resolveProjectAsset(project, config.entitlements) : null;
631
+ if (entitlements) args.push('--entitlements', entitlements);
632
+ args.push('--sign', config.signingIdentity || '-', targetPath);
633
+ return args;
634
+ }
635
+
636
+ async function customizeWindowsExecutable({ project, executablePath, productName }) {
637
+ const config = project.config.build.windows;
638
+ let rceditModule;
639
+ try {
640
+ rceditModule = require('rcedit');
641
+ } catch (error) {
642
+ console.warn(`AtomJS could not load rcedit, so Windows executable metadata was not customized: ${error.message}`);
643
+ return;
644
+ }
645
+
646
+ const edit = rceditModule.rcedit || rceditModule.default || rceditModule;
647
+ const version = normalizeWindowsVersion(project.packageJson.version);
648
+ const author = project.packageJson.author;
649
+ const packagePublisher = typeof author === 'string'
650
+ ? author
651
+ : author && typeof author === 'object' && author.name
652
+ ? author.name
653
+ : 'AtomJS application';
654
+ const publisher = config.publisher || packagePublisher;
655
+ const iconSource = resolveProjectAsset(project, config.icon, '.ico');
656
+ const options = {
657
+ 'file-version': version,
658
+ 'product-version': version,
659
+ 'requested-execution-level': config.requestedExecutionLevel,
660
+ 'version-string': {
661
+ CompanyName: publisher,
662
+ FileDescription: productName,
663
+ ProductName: productName,
664
+ InternalName: productName,
665
+ OriginalFilename: path.basename(executablePath),
666
+ LegalCopyright: typeof project.packageJson.license === 'string' ? project.packageJson.license : ''
667
+ }
668
+ };
669
+ if (iconSource) options.icon = iconSource;
670
+
671
+ try {
672
+ await new Promise((resolve, reject) => {
673
+ let settled = false;
674
+ const finish = (error) => {
675
+ if (settled) return;
676
+ settled = true;
677
+ if (error) reject(error);
678
+ else resolve();
679
+ };
680
+ let result;
681
+ try {
682
+ result = edit(executablePath, options, finish);
683
+ } catch (error) {
684
+ finish(error);
685
+ return;
686
+ }
687
+ if (result && typeof result.then === 'function') result.then(() => finish(), finish);
688
+ });
689
+ } catch (error) {
690
+ throw new Error(`AtomJS could not customize the Windows executable: ${error.message}`);
691
+ }
692
+ }
693
+
481
694
  async function packageWindows({ project, buildRoot, unpacked, executableName, productName }) {
482
695
  const outputs = [];
483
- const zipPath = path.join(buildRoot, `${productName}-windows.zip`);
696
+ const config = project.config.build.windows;
697
+ const artifactBase = renderArtifactBase(project, 'windows');
698
+ const zipPath = path.join(buildRoot, `${artifactBase}-portable.zip`);
484
699
  await archiveDirectory(unpacked, zipPath, 'zip');
485
700
  outputs.push(zipPath);
486
701
 
487
702
  const nsisPath = path.join(buildRoot, 'installer.nsi');
488
- const installerPath = path.join(buildRoot, `${productName} Installer.exe`);
703
+ const installerPath = path.join(buildRoot, `${artifactBase}-setup.exe`);
489
704
  const escapedSource = unpacked.replace(/\\/g, '\\\\');
490
705
  const version = String(project.packageJson.version || '0.0.0');
491
706
  const author = project.packageJson.author;
492
- const publisher = typeof author === 'string'
707
+ const packagePublisher = typeof author === 'string'
493
708
  ? author
494
709
  : author && typeof author === 'object' && author.name
495
710
  ? author.name
496
711
  : 'AtomJS application';
712
+ const publisher = config.publisher || packagePublisher;
497
713
  const uninstallKey = `Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${escapeNsis(project.config.appId)}`;
714
+ const installerIcon = resolveProjectAsset(project, config.installerIcon, '.ico');
715
+ const headerImage = resolveProjectAsset(project, config.headerImage, '.bmp');
716
+ const sidebarImage = resolveProjectAsset(project, config.sidebarImage, '.bmp');
717
+ const installMode = config.installMode === 'machine' ? 'machine' : 'user';
718
+ const requestLevel = installMode === 'machine' ? 'admin' : 'user';
719
+ const defaultInstallDir = installMode === 'machine'
720
+ ? `$PROGRAMFILES64\\${escapeNsis(productName)}`
721
+ : `$LOCALAPPDATA\\Programs\\${escapeNsis(productName)}`;
722
+ const installDir = config.installDirectory
723
+ ? escapeNsis(config.installDirectory)
724
+ : defaultInstallDir;
725
+ const registryRoot = installMode === 'machine' ? 'HKLM' : 'HKCU';
726
+ const shellContext = installMode === 'machine' ? 'all' : 'current';
498
727
  const credit = project.config.installerCredit
499
728
  ? `!define MUI_WELCOMEPAGE_TEXT "This installer will install ${escapeNsis(productName)}.$\\r$\\n$\\r$\\nPowered by AtomJS — https://github.com/Atom-js-org/atom"`
500
729
  : '';
730
+ const welcomeText = config.welcomeText
731
+ ? `!define MUI_WELCOMEPAGE_TEXT "${escapeNsis(config.welcomeText)}"`
732
+ : credit;
733
+ const finishText = config.finishText
734
+ ? `!define MUI_FINISHPAGE_TEXT "${escapeNsis(config.finishText)}"`
735
+ : '';
736
+ const iconDirectives = installerIcon
737
+ ? `Icon "${installerIcon.replace(/\\/g, '\\\\')}"\n!define MUI_ICON "${installerIcon.replace(/\\/g, '\\\\')}"\n!define MUI_UNICON "${installerIcon.replace(/\\/g, '\\\\')}"`
738
+ : '';
739
+ const headerDirectives = headerImage
740
+ ? `!define MUI_HEADERIMAGE\n!define MUI_HEADERIMAGE_BITMAP "${headerImage.replace(/\\/g, '\\\\')}"`
741
+ : '';
742
+ const sidebarDirectives = sidebarImage
743
+ ? `!define MUI_WELCOMEFINISHPAGE_BITMAP "${sidebarImage.replace(/\\/g, '\\\\')}"`
744
+ : '';
745
+ const directoryPage = config.allowDirectorySelection ? '!insertmacro MUI_PAGE_DIRECTORY' : '';
746
+ const finishRun = config.runAfterFinish
747
+ ? `!define MUI_FINISHPAGE_RUN "$INSTDIR\\${escapeNsis(executableName)}"`
748
+ : '';
749
+ const desktopShortcut = config.createDesktopShortcut
750
+ ? `CreateShortcut "$DESKTOP\\${escapeNsis(productName)}.lnk" "$INSTDIR\\${escapeNsis(executableName)}"`
751
+ : '';
752
+ const startMenuInstall = config.createStartMenuShortcut
753
+ ? `CreateDirectory "$SMPROGRAMS\\${escapeNsis(productName)}"\n CreateShortcut "$SMPROGRAMS\\${escapeNsis(productName)}\\${escapeNsis(productName)}.lnk" "$INSTDIR\\${escapeNsis(executableName)}"`
754
+ : '';
755
+ const startMenuRemove = config.createStartMenuShortcut
756
+ ? `RMDir /r "$SMPROGRAMS\\${escapeNsis(productName)}"`
757
+ : '';
758
+ const desktopRemove = config.createDesktopShortcut
759
+ ? `Delete "$DESKTOP\\${escapeNsis(productName)}.lnk"`
760
+ : '';
501
761
  const script = `
502
762
  Unicode true
503
763
  SetCompressor /SOLID lzma
504
764
  !include "MUI2.nsh"
505
765
  Name "${escapeNsis(productName)}"
506
766
  OutFile "${installerPath.replace(/\\/g, '\\\\')}"
507
- InstallDir "$LOCALAPPDATA\\Programs\\${escapeNsis(productName)}"
508
- RequestExecutionLevel user
767
+ InstallDir "${installDir}"
768
+ RequestExecutionLevel ${requestLevel}
509
769
  ShowInstDetails show
510
770
  ShowUninstDetails show
511
- ${credit}
771
+ ${iconDirectives}
772
+ ${headerDirectives}
773
+ ${sidebarDirectives}
774
+ ${welcomeText}
775
+ ${finishText}
512
776
  !insertmacro MUI_PAGE_WELCOME
513
- !insertmacro MUI_PAGE_DIRECTORY
777
+ ${directoryPage}
514
778
  !insertmacro MUI_PAGE_INSTFILES
515
- !define MUI_FINISHPAGE_RUN "$INSTDIR\\${escapeNsis(executableName)}"
779
+ ${finishRun}
516
780
  !insertmacro MUI_PAGE_FINISH
517
781
  !insertmacro MUI_UNPAGE_CONFIRM
518
782
  !insertmacro MUI_UNPAGE_INSTFILES
519
- !insertmacro MUI_LANGUAGE "English"
783
+ !insertmacro MUI_LANGUAGE "${escapeNsis(config.language)}"
520
784
  Section "Install"
521
- SetShellVarContext current
785
+ SetShellVarContext ${shellContext}
522
786
  SetOutPath "$INSTDIR"
523
787
  File /r "${escapedSource}\\*"
524
- CreateDirectory "$SMPROGRAMS\\${escapeNsis(productName)}"
525
- CreateShortcut "$SMPROGRAMS\\${escapeNsis(productName)}\\${escapeNsis(productName)}.lnk" "$INSTDIR\\${escapeNsis(executableName)}"
526
- CreateShortcut "$DESKTOP\\${escapeNsis(productName)}.lnk" "$INSTDIR\\${escapeNsis(executableName)}"
788
+ ${startMenuInstall}
789
+ ${desktopShortcut}
527
790
  WriteUninstaller "$INSTDIR\\Uninstall.exe"
528
- WriteRegStr HKCU "${uninstallKey}" "DisplayName" "${escapeNsis(productName)}"
529
- WriteRegStr HKCU "${uninstallKey}" "DisplayVersion" "${escapeNsis(version)}"
530
- WriteRegStr HKCU "${uninstallKey}" "Publisher" "${escapeNsis(publisher)}"
531
- WriteRegStr HKCU "${uninstallKey}" "DisplayIcon" "$INSTDIR\\${escapeNsis(executableName)}"
532
- WriteRegStr HKCU "${uninstallKey}" "UninstallString" "$\\\"$INSTDIR\\Uninstall.exe$\\\""
533
- WriteRegDWORD HKCU "${uninstallKey}" "NoModify" 1
534
- WriteRegDWORD HKCU "${uninstallKey}" "NoRepair" 1
791
+ WriteRegStr ${registryRoot} "${uninstallKey}" "DisplayName" "${escapeNsis(productName)}"
792
+ WriteRegStr ${registryRoot} "${uninstallKey}" "DisplayVersion" "${escapeNsis(version)}"
793
+ WriteRegStr ${registryRoot} "${uninstallKey}" "Publisher" "${escapeNsis(publisher)}"
794
+ WriteRegStr ${registryRoot} "${uninstallKey}" "DisplayIcon" "$INSTDIR\\${escapeNsis(executableName)}"
795
+ WriteRegStr ${registryRoot} "${uninstallKey}" "UninstallString" "$\\\"$INSTDIR\\Uninstall.exe$\\\""
796
+ WriteRegDWORD ${registryRoot} "${uninstallKey}" "NoModify" 1
797
+ WriteRegDWORD ${registryRoot} "${uninstallKey}" "NoRepair" 1
535
798
  SectionEnd
536
799
  Section "Uninstall"
537
- SetShellVarContext current
538
- Delete "$DESKTOP\\${escapeNsis(productName)}.lnk"
539
- RMDir /r "$SMPROGRAMS\\${escapeNsis(productName)}"
540
- DeleteRegKey HKCU "${uninstallKey}"
800
+ SetShellVarContext ${shellContext}
801
+ ${desktopRemove}
802
+ ${startMenuRemove}
803
+ DeleteRegKey ${registryRoot} "${uninstallKey}"
541
804
  RMDir /r "$INSTDIR"
542
805
  SectionEnd
543
806
  `;
@@ -619,10 +882,12 @@ async function sanitizeMacBundle(appBundle) {
619
882
 
620
883
  async function packageMacOS({ project, buildRoot, unpacked, executableName, productName, hostSource }) {
621
884
  const outputs = [];
622
- const finalAppBundle = path.join(buildRoot, `${productName}.app`);
885
+ const config = project.config.build.macos;
886
+ const bundleName = sanitizeFilename(config.bundleName || productName);
887
+ const finalAppBundle = path.join(buildRoot, `${bundleName}.app`);
623
888
  const stageBase = await resolveShortStageBase();
624
889
  const bundleStageRoot = await fs.promises.mkdtemp(path.join(stageBase, 'macos-app-'));
625
- const appBundle = path.join(bundleStageRoot, `${productName}.app`);
890
+ const appBundle = path.join(bundleStageRoot, `${bundleName}.app`);
626
891
  const contents = path.join(appBundle, 'Contents');
627
892
  const macosDir = path.join(contents, 'MacOS');
628
893
  const resources = path.join(contents, 'Resources');
@@ -647,7 +912,7 @@ async function packageMacOS({ project, buildRoot, unpacked, executableName, prod
647
912
  'clang',
648
913
  '-fobjc-arc',
649
914
  '-fmodules',
650
- '-mmacosx-version-min=12.0',
915
+ `-mmacosx-version-min=${config.minimumSystemVersion}`,
651
916
  '-framework', 'Cocoa',
652
917
  '-framework', 'WebKit',
653
918
  hostSource,
@@ -655,12 +920,16 @@ async function packageMacOS({ project, buildRoot, unpacked, executableName, prod
655
920
  ]);
656
921
  await fs.promises.chmod(nativeHost, 0o755);
657
922
 
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
- }
923
+ const preparedIcon = await prepareMacIcon(project, resources, bundleStageRoot);
924
+ const iconEntry = preparedIcon
925
+ ? '<key>CFBundleIconFile</key><string>AppIcon</string>'
926
+ : '';
927
+ const categoryEntry = config.category
928
+ ? `<key>LSApplicationCategoryType</key><string>${xml(config.category)}</string>`
929
+ : '';
930
+ const copyrightEntry = config.copyright
931
+ ? `<key>NSHumanReadableCopyright</key><string>${xml(config.copyright)}</string>`
932
+ : '';
664
933
 
665
934
  const version = String(project.packageJson.version || '0.0.0');
666
935
  const bundleVersion = (version.match(/\d+/g) || ['1']).slice(0, 3).join('.');
@@ -669,13 +938,15 @@ async function packageMacOS({ project, buildRoot, unpacked, executableName, prod
669
938
  <plist version="1.0"><dict>
670
939
  <key>CFBundleExecutable</key><string>${xml(productName)}</string>
671
940
  <key>CFBundleIdentifier</key><string>${xml(project.config.appId)}</string>
672
- <key>CFBundleName</key><string>${xml(productName)}</string>
941
+ <key>CFBundleName</key><string>${xml(bundleName)}</string>
673
942
  <key>CFBundleDisplayName</key><string>${xml(productName)}</string>
674
943
  <key>CFBundlePackageType</key><string>APPL</string>
675
944
  <key>CFBundleShortVersionString</key><string>${xml(version)}</string>
676
945
  <key>CFBundleVersion</key><string>${xml(bundleVersion)}</string>
677
- <key>LSMinimumSystemVersion</key><string>12.0</string>
946
+ <key>LSMinimumSystemVersion</key><string>${xml(config.minimumSystemVersion)}</string>
678
947
  <key>NSHighResolutionCapable</key><true/>
948
+ ${categoryEntry}
949
+ ${copyrightEntry}
679
950
  ${iconEntry}
680
951
  </dict></plist>`;
681
952
  const plistPath = path.join(contents, 'Info.plist');
@@ -689,19 +960,13 @@ ${iconEntry}
689
960
 
690
961
  if (hasMacCodeSigningTool()) {
691
962
  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 });
694
-
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.
963
+ await run('/usr/bin/codesign', codesignArguments(project, nativeHost), { env: signingEnvironment });
964
+ await run('/usr/bin/codesign', codesignArguments(project, mainExecutable), { env: signingEnvironment });
697
965
  await sanitizeMacBundle(appBundle);
698
- await run('/usr/bin/codesign', ['--force', '--deep', '--sign', '-', appBundle], { env: signingEnvironment });
966
+ await run('/usr/bin/codesign', codesignArguments(project, appBundle, true), { env: signingEnvironment });
699
967
  await run('/usr/bin/codesign', ['--verify', '--deep', '--strict', '--verbose=2', appBundle], { env: signingEnvironment });
700
968
  }
701
969
 
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
970
  await fse.remove(finalAppBundle);
706
971
  await fse.copy(appBundle, finalAppBundle);
707
972
  await fs.promises.chmod(path.join(finalAppBundle, 'Contents', 'MacOS', productName), 0o755);
@@ -714,35 +979,44 @@ ${iconEntry}
714
979
  });
715
980
  }
716
981
 
717
- const zipPath = path.join(buildRoot, `${productName}-macos.zip`);
982
+ const artifactBase = renderArtifactBase(project, 'macos');
983
+ const zipPath = path.join(buildRoot, `${artifactBase}.zip`);
718
984
  if (commandExists('ditto', ['-h'])) {
719
985
  await run('ditto', ['--norsrc', '-c', '-k', '--keepParent', finalAppBundle, zipPath], {
720
986
  env: { ...process.env, COPYFILE_DISABLE: '1' }
721
987
  });
722
988
  } else {
723
- await archiveDirectory(finalAppBundle, zipPath, 'zip', `${productName}.app`);
989
+ await archiveDirectory(finalAppBundle, zipPath, 'zip', `${bundleName}.app`);
724
990
  }
725
991
  outputs.push(zipPath);
726
992
 
727
- if (commandExists('/usr/bin/hdiutil', ['help'])) {
728
- const dmgPath = path.join(buildRoot, `${productName}.dmg`);
729
- const temporaryDmgPath = path.join(bundleStageRoot, `${productName}.dmg`);
993
+ if (config.dmg.enabled && commandExists('/usr/bin/hdiutil', ['help'])) {
994
+ const dmgBase = renderArtifactBase(project, 'macos', config.dmg.artifactName || `${artifactBase}-installer`);
995
+ const dmgPath = path.join(buildRoot, `${dmgBase}.dmg`);
996
+ const temporaryDmgPath = path.join(bundleStageRoot, `${dmgBase}.dmg`);
997
+ const dmgRoot = path.join(bundleStageRoot, 'dmg-root');
730
998
 
731
999
  try {
732
- // hdiutil can reject both source and output paths located on external,
733
- // removable, network, or non-APFS volumes. Create the image entirely on
734
- // the local temporary filesystem, then copy the finished DMG to build/.
1000
+ await fse.remove(dmgRoot);
1001
+ await fse.ensureDir(dmgRoot);
1002
+ await fse.copy(appBundle, path.join(dmgRoot, `${bundleName}.app`));
1003
+ await fs.promises.symlink('/Applications', path.join(dmgRoot, 'Applications'));
1004
+ if (config.dmg.background) {
1005
+ const background = resolveProjectAsset(project, config.dmg.background);
1006
+ const backgroundDirectory = path.join(dmgRoot, '.background');
1007
+ await fse.ensureDir(backgroundDirectory);
1008
+ await fse.copy(background, path.join(backgroundDirectory, path.basename(background)));
1009
+ }
1010
+ await sanitizeMacBundle(dmgRoot);
735
1011
  await fse.remove(temporaryDmgPath);
736
1012
  await run('/usr/bin/hdiutil', [
737
1013
  'create',
738
- '-volname', productName,
739
- '-srcfolder', appBundle,
1014
+ '-volname', config.dmg.volumeName,
1015
+ '-srcfolder', dmgRoot,
740
1016
  '-ov',
741
1017
  '-format', 'UDZO',
742
1018
  temporaryDmgPath
743
- ], {
744
- env: { ...process.env, COPYFILE_DISABLE: '1' }
745
- });
1019
+ ], { env: { ...process.env, COPYFILE_DISABLE: '1' } });
746
1020
 
747
1021
  await fse.remove(dmgPath);
748
1022
  await fs.promises.copyFile(temporaryDmgPath, dmgPath);
@@ -750,7 +1024,6 @@ ${iconEntry}
750
1024
  } catch (error) {
751
1025
  await fse.remove(temporaryDmgPath);
752
1026
  await fse.remove(dmgPath);
753
-
754
1027
  if (process.env.ATOM_REQUIRE_DMG === '1') throw error;
755
1028
  console.warn(`AtomJS could not create the optional macOS DMG; the signed .app and ZIP are still valid. ${error.message}`);
756
1029
  }
@@ -764,40 +1037,223 @@ ${iconEntry}
764
1037
 
765
1038
  async function packageLinux({ project, buildRoot, unpacked, executableName, productName }) {
766
1039
  const outputs = [];
767
- const tarPath = path.join(buildRoot, `${productName}-linux.tar.gz`);
768
- await archiveDirectory(unpacked, tarPath, 'tar');
1040
+ const config = project.config.build.linux;
1041
+ const artifactBase = renderArtifactBase(project, 'linux');
1042
+ const binaryName = sanitizeFilename(config.binaryName).replace(/\s+/g, '-');
1043
+ const packageName = String(config.packageName).toLowerCase().replace(/[^a-z0-9+.-]+/g, '-') || 'atomjs-app';
1044
+ const sourceExecutable = path.join(unpacked, executableName);
1045
+
1046
+ const portableBinary = path.join(buildRoot, binaryName);
1047
+ await fse.copy(sourceExecutable, portableBinary);
1048
+ await fs.promises.chmod(portableBinary, 0o755);
1049
+ outputs.push(portableBinary);
1050
+
1051
+ const tarPath = path.join(buildRoot, `${artifactBase}-portable.tar.gz`);
1052
+ await archiveDirectory(unpacked, tarPath, 'tar', productName);
769
1053
  outputs.push(tarPath);
770
1054
 
771
1055
  const appDir = path.join(buildRoot, `${productName}.AppDir`);
772
1056
  const usrBin = path.join(appDir, 'usr', 'bin');
773
- const usrLibApp = path.join(appDir, 'usr', 'lib', sanitizeFilename(project.packageJson.name || productName));
1057
+ const applicationsDir = path.join(appDir, 'usr', 'share', 'applications');
1058
+ const iconsDir = path.join(appDir, 'usr', 'share', 'icons', 'hicolor', '512x512', 'apps');
1059
+ const docsDir = path.join(appDir, 'usr', 'share', 'doc', packageName);
1060
+ await fse.remove(appDir);
774
1061
  await fse.ensureDir(usrBin);
775
- await fse.ensureDir(usrLibApp);
776
- await fse.copy(path.join(unpacked, executableName), path.join(usrBin, productName));
777
- await fse.copy(path.join(unpacked, 'ATOMJS-CREDIT.txt'), path.join(usrLibApp, 'ATOMJS-CREDIT.txt'));
1062
+ await fse.ensureDir(applicationsDir);
1063
+ await fse.ensureDir(docsDir);
1064
+ await fse.copy(sourceExecutable, path.join(usrBin, binaryName));
1065
+ await fs.promises.chmod(path.join(usrBin, binaryName), 0o755);
1066
+ await fse.copy(path.join(unpacked, 'ATOMJS-CREDIT.txt'), path.join(docsDir, 'ATOMJS-CREDIT.txt'));
778
1067
 
779
- const appRun = `#!/bin/sh\nHERE="$(dirname "$(readlink -f "$0")")"\nexec "$HERE/usr/bin/${productName}" "$@"\n`;
1068
+ const appRun = `#!/bin/sh\nHERE="$(dirname "$(readlink -f "$0")")"\nexec "$HERE/usr/bin/${binaryName}" "$@"\n`;
780
1069
  await fs.promises.writeFile(path.join(appDir, 'AppRun'), appRun, { mode: 0o755 });
781
- const desktopName = sanitizeFilename(project.packageJson.name || productName).toLowerCase().replace(/\s+/g, '-');
782
- const desktop = `[Desktop Entry]\nType=Application\nName=${productName}\nExec=${productName}\nIcon=${desktopName}\nCategories=Development;Utility;\nTerminal=false\n`;
783
- await fs.promises.writeFile(path.join(appDir, `${desktopName}.desktop`), desktop, 'utf8');
1070
+ const desktop = `[Desktop Entry]\nType=Application\nName=${productName}\nComment=${config.description}\nExec=${binaryName}\nIcon=${packageName}\nCategories=${config.category};\nTerminal=false\nStartupNotify=true\n`;
1071
+ await fs.promises.writeFile(path.join(applicationsDir, `${packageName}.desktop`), desktop, 'utf8');
1072
+ await fse.copy(path.join(applicationsDir, `${packageName}.desktop`), path.join(appDir, `${packageName}.desktop`));
1073
+
1074
+ const iconSource = config.icon ? resolveProjectAsset(project, config.icon) : null;
1075
+ if (iconSource) {
1076
+ if (path.extname(iconSource).toLowerCase() !== '.png') {
1077
+ throw new Error(`AtomJS Linux icons must be PNG files: ${iconSource}`);
1078
+ }
1079
+ await fse.ensureDir(iconsDir);
1080
+ await fse.copy(iconSource, path.join(iconsDir, `${packageName}.png`));
1081
+ await fse.copy(iconSource, path.join(appDir, `${packageName}.png`));
1082
+ }
1083
+
1084
+ if (config.appImage) {
1085
+ const tool = findAppImageTool();
1086
+ if (tool) {
1087
+ const appImagePath = path.join(buildRoot, `${artifactBase}.AppImage`);
1088
+ await run(tool, [appDir, appImagePath], {
1089
+ env: { ...process.env, ARCH: process.arch === 'arm64' ? 'aarch64' : 'x86_64' }
1090
+ });
1091
+ outputs.push(appImagePath);
1092
+ } else {
1093
+ console.warn('appimagetool was not found; the portable binary, AppDir and .deb are still available.');
1094
+ }
1095
+ }
784
1096
 
785
- const iconSource = project.config.icon ? path.resolve(project.root, project.config.icon) : null;
786
- if (iconSource && fs.existsSync(iconSource)) {
787
- await fse.copy(iconSource, path.join(appDir, `${desktopName}.png`));
1097
+ if (config.deb) {
1098
+ const debStage = path.join(buildRoot, '.deb-stage');
1099
+ const controlRoot = path.join(debStage, 'control');
1100
+ const dataRoot = path.join(debStage, 'data');
1101
+ await fse.remove(debStage);
1102
+ await fse.ensureDir(controlRoot);
1103
+ await fse.ensureDir(path.join(dataRoot, 'usr', 'bin'));
1104
+ await fse.ensureDir(path.join(dataRoot, 'usr', 'share', 'applications'));
1105
+ await fse.ensureDir(path.join(dataRoot, 'usr', 'share', 'doc', packageName));
1106
+
1107
+ await fse.copy(sourceExecutable, path.join(dataRoot, 'usr', 'bin', binaryName));
1108
+ await fs.promises.chmod(path.join(dataRoot, 'usr', 'bin', binaryName), 0o755);
1109
+ await fse.copy(
1110
+ path.join(applicationsDir, `${packageName}.desktop`),
1111
+ path.join(dataRoot, 'usr', 'share', 'applications', `${packageName}.desktop`)
1112
+ );
1113
+ await fse.copy(
1114
+ path.join(unpacked, 'ATOMJS-CREDIT.txt'),
1115
+ path.join(dataRoot, 'usr', 'share', 'doc', packageName, 'ATOMJS-CREDIT.txt')
1116
+ );
1117
+ if (iconSource) {
1118
+ const debIconDir = path.join(dataRoot, 'usr', 'share', 'icons', 'hicolor', '512x512', 'apps');
1119
+ await fse.ensureDir(debIconDir);
1120
+ await fse.copy(iconSource, path.join(debIconDir, `${packageName}.png`));
1121
+ }
1122
+
1123
+ const dependencies = config.dependencies.length > 0 ? `\nDepends: ${config.dependencies.join(', ')}` : '';
1124
+ const control = [
1125
+ `Package: ${packageName}`,
1126
+ `Version: ${normalizeDebVersion(project.packageJson.version)}`,
1127
+ 'Section: utils',
1128
+ 'Priority: optional',
1129
+ `Architecture: ${debArchitecture()}`,
1130
+ `Maintainer: ${config.maintainer || packageAuthor(project)}`,
1131
+ `Description: ${String(config.description).replace(/\r?\n/g, '\n ')}`
1132
+ ].join('\n') + dependencies + '\n';
1133
+ await fs.promises.writeFile(path.join(controlRoot, 'control'), control, 'utf8');
1134
+
1135
+ const controlTar = path.join(debStage, 'control.tar.gz');
1136
+ const dataTar = path.join(debStage, 'data.tar.gz');
1137
+ await archiveDirectory(controlRoot, controlTar, 'tar');
1138
+ await archiveDirectory(dataRoot, dataTar, 'tar');
1139
+ const debPath = path.join(buildRoot, `${artifactBase}.deb`);
1140
+ await writeArArchive(debPath, [
1141
+ { name: 'debian-binary', data: Buffer.from('2.0\n') },
1142
+ { name: 'control.tar.gz', data: await fs.promises.readFile(controlTar) },
1143
+ { name: 'data.tar.gz', data: await fs.promises.readFile(dataTar) }
1144
+ ]);
1145
+ outputs.push(debPath);
1146
+ await fse.remove(debStage);
788
1147
  }
789
1148
 
790
- const tool = findAppImageTool();
791
- if (tool) {
792
- const appImagePath = path.join(buildRoot, `${productName}.AppImage`);
793
- await run(tool, [appDir, appImagePath], { env: { ...process.env, ARCH: process.arch === 'arm64' ? 'aarch64' : 'x86_64' } });
794
- outputs.push(appImagePath);
795
- } else {
796
- console.warn('appimagetool was not found; AppDir was created but AppImage generation was skipped.');
1149
+ if (config.rpm) {
1150
+ if (commandExists('rpmbuild', ['--version'])) {
1151
+ const rpmStage = path.join(buildRoot, '.rpm-stage');
1152
+ const rpmTop = path.join(rpmStage, 'rpmbuild');
1153
+ const rpmSourceRoot = path.join(rpmStage, 'payload');
1154
+ const rpmSpecPath = path.join(rpmTop, 'SPECS', `${packageName}.spec`);
1155
+ const rpmSourcePath = path.join(rpmTop, 'SOURCES', `${packageName}-payload.tar.gz`);
1156
+ const rpmBinaryPath = path.join(rpmSourceRoot, 'usr', 'bin', binaryName);
1157
+ const rpmDesktopPath = path.join(rpmSourceRoot, 'usr', 'share', 'applications', `${packageName}.desktop`);
1158
+ const rpmDocsPath = path.join(rpmSourceRoot, 'usr', 'share', 'doc', packageName, 'ATOMJS-CREDIT.txt');
1159
+
1160
+ await fse.remove(rpmStage);
1161
+ for (const directory of ['BUILD', 'BUILDROOT', 'RPMS', 'SOURCES', 'SPECS', 'SRPMS']) {
1162
+ await fse.ensureDir(path.join(rpmTop, directory));
1163
+ }
1164
+ await fse.ensureDir(path.dirname(rpmBinaryPath));
1165
+ await fse.ensureDir(path.dirname(rpmDesktopPath));
1166
+ await fse.ensureDir(path.dirname(rpmDocsPath));
1167
+ await fse.copy(sourceExecutable, rpmBinaryPath);
1168
+ await fs.promises.chmod(rpmBinaryPath, 0o755);
1169
+ await fse.copy(path.join(applicationsDir, `${packageName}.desktop`), rpmDesktopPath);
1170
+ await fse.copy(path.join(unpacked, 'ATOMJS-CREDIT.txt'), rpmDocsPath);
1171
+
1172
+ const rpmFiles = [
1173
+ `/usr/bin/${binaryName}`,
1174
+ `/usr/share/applications/${packageName}.desktop`,
1175
+ `/usr/share/doc/${packageName}/ATOMJS-CREDIT.txt`
1176
+ ];
1177
+ if (iconSource) {
1178
+ const rpmIconPath = path.join(rpmSourceRoot, 'usr', 'share', 'icons', 'hicolor', '512x512', 'apps', `${packageName}.png`);
1179
+ await fse.ensureDir(path.dirname(rpmIconPath));
1180
+ await fse.copy(iconSource, rpmIconPath);
1181
+ rpmFiles.push(`/usr/share/icons/hicolor/512x512/apps/${packageName}.png`);
1182
+ }
1183
+
1184
+ await archiveDirectory(rpmSourceRoot, rpmSourcePath, 'tar');
1185
+ const rpmRequires = config.rpmDependencies.length > 0
1186
+ ? `Requires: ${config.rpmDependencies.join(', ')}`
1187
+ : '';
1188
+ const rpmSpec = [
1189
+ `Name: ${packageName}`,
1190
+ `Version: ${normalizeRpmVersion(project.packageJson.version)}`,
1191
+ 'Release: 1%{?dist}',
1192
+ `Summary: ${rpmText(config.description || productName)}`,
1193
+ `License: ${rpmText(project.packageJson.license || 'Proprietary')}`,
1194
+ `BuildArch: ${rpmArchitecture()}`,
1195
+ 'Source0: ' + path.basename(rpmSourcePath),
1196
+ 'AutoReqProv: no',
1197
+ rpmRequires,
1198
+ '',
1199
+ '%description',
1200
+ rpmText(config.description || productName),
1201
+ '',
1202
+ '%prep',
1203
+ '',
1204
+ '%build',
1205
+ '',
1206
+ '%install',
1207
+ 'rm -rf %{buildroot}',
1208
+ 'mkdir -p %{buildroot}',
1209
+ 'tar -xzf %{SOURCE0} -C %{buildroot}',
1210
+ '',
1211
+ '%files',
1212
+ ...rpmFiles,
1213
+ ''
1214
+ ].filter((line, index, lines) => line !== '' || lines[index - 1] !== '').join('\n');
1215
+ await fs.promises.writeFile(rpmSpecPath, rpmSpec, 'utf8');
1216
+
1217
+ await run('rpmbuild', ['--define', `_topdir ${rpmTop}`, '-bb', rpmSpecPath]);
1218
+ const rpmCandidates = [];
1219
+ async function findRpmFiles(directory) {
1220
+ for (const entry of await fs.promises.readdir(directory, { withFileTypes: true })) {
1221
+ const absolute = path.join(directory, entry.name);
1222
+ if (entry.isDirectory()) await findRpmFiles(absolute);
1223
+ else if (entry.isFile() && entry.name.endsWith('.rpm')) rpmCandidates.push(absolute);
1224
+ }
1225
+ }
1226
+ await findRpmFiles(path.join(rpmTop, 'RPMS'));
1227
+ if (rpmCandidates.length === 0) throw new Error('rpmbuild completed without producing an RPM package.');
1228
+ const rpmPath = path.join(buildRoot, `${artifactBase}.rpm`);
1229
+ await fse.copy(rpmCandidates[0], rpmPath);
1230
+ outputs.push(rpmPath);
1231
+ await fse.remove(rpmStage);
1232
+ } else {
1233
+ console.warn('rpmbuild was not found; the portable binary, AppImage and .deb remain available.');
1234
+ }
797
1235
  }
1236
+
1237
+ const distro = detectLinuxDistribution();
1238
+ if (distro) console.log(`Linux packaging host: ${distro}`);
798
1239
  return outputs;
799
1240
  }
800
1241
 
1242
+ function detectLinuxDistribution() {
1243
+ if (process.platform !== 'linux' || !fs.existsSync('/etc/os-release')) return null;
1244
+ try {
1245
+ const values = {};
1246
+ for (const line of fs.readFileSync('/etc/os-release', 'utf8').split(/\r?\n/)) {
1247
+ const match = line.match(/^([A-Z_]+)=(.*)$/);
1248
+ if (!match) continue;
1249
+ values[match[1]] = match[2].replace(/^"|"$/g, '');
1250
+ }
1251
+ return values.PRETTY_NAME || values.NAME || values.ID || null;
1252
+ } catch {
1253
+ return null;
1254
+ }
1255
+ }
1256
+
801
1257
  function findAppImageTool() {
802
1258
  for (const name of ['appimagetool', 'appimagetool.AppImage']) {
803
1259
  if (commandExists(name, ['--version'])) return name;
@@ -912,4 +1368,4 @@ function xml(value) {
912
1368
  return String(value).replace(/[<>&'\"]/g, (char) => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;', "'": '&apos;', '"': '&quot;' })[char]);
913
1369
  }
914
1370
 
915
- module.exports = { buildCommand, localBuild, createApplicationPayload, readPortableExecutableLayout, removeMacMetadataFiles };
1371
+ module.exports = { buildCommand, localBuild, createApplicationPayload, readPortableExecutableLayout, removeMacMetadataFiles, writeArArchive, normalizeDebVersion, normalizeRpmVersion };
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.5-alpha.0',
34
- electron: 'npm:@atom-js-org/electron@0.4.5-alpha.0'
33
+ '@atom-js-org/runtime': '0.5.0-alpha.0',
34
+ electron: 'npm:@atom-js-org/electron@0.5.0-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.5-alpha.0'
40
+ '@atom-js-org/cli': '0.5.0-alpha.0'
41
41
  },
42
42
  overrides: {
43
43
  tar: '7.5.20'
@@ -50,12 +50,45 @@ async function initCommand(directory, options = {}) {
50
50
  main: 'src/main.js',
51
51
  icon: 'assets/icon.png',
52
52
  installerCredit: true,
53
+ build: {
54
+ artifactName: '${productName}-${version}-${target}-${arch}',
55
+ windows: {
56
+ icon: 'assets/icon.ico',
57
+ installerIcon: 'assets/icon.ico',
58
+ installMode: 'user',
59
+ createDesktopShortcut: true,
60
+ createStartMenuShortcut: true,
61
+ allowDirectorySelection: true,
62
+ runAfterFinish: true
63
+ },
64
+ macos: {
65
+ icon: 'assets/icon.png',
66
+ bundleName: productName,
67
+ category: 'public.app-category.utilities',
68
+ minimumSystemVersion: '12.0',
69
+ dmg: {
70
+ enabled: true,
71
+ volumeName: productName
72
+ }
73
+ },
74
+ linux: {
75
+ icon: 'assets/icon.png',
76
+ binaryName: packageName,
77
+ packageName,
78
+ category: 'Utility',
79
+ appImage: true,
80
+ deb: true,
81
+ rpm: true
82
+ }
83
+ },
53
84
  github: {
54
85
  workflow: 'atom-build.yml'
55
86
  }
56
87
  }, null, 2));
57
88
 
58
89
  await fse.ensureDir(path.join(root, 'assets'));
90
+ await fse.copy(path.join(__dirname, '..', 'assets', 'default-icon.png'), path.join(root, 'assets', 'icon.png'));
91
+ await fse.copy(path.join(__dirname, '..', 'assets', 'default-icon.ico'), path.join(root, 'assets', 'icon.ico'));
59
92
  await fs.promises.writeFile(path.join(root, 'src', 'main.js'), mainTemplate());
60
93
  await fs.promises.writeFile(path.join(root, 'src', 'preload.js'), preloadTemplate());
61
94
  await fs.promises.writeFile(path.join(root, 'src', 'index.html'), htmlTemplate(productName));
@@ -213,7 +246,7 @@ jobs:
213
246
  runs-on: ubuntu-latest
214
247
  steps:
215
248
  - uses: actions/checkout@v4
216
- - run: sudo apt-get update && sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev zenity
249
+ - run: sudo apt-get update && sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev zenity rpm
217
250
  - uses: actions/setup-node@v4
218
251
  with:
219
252
  node-version: 22
package/src/utils.cjs CHANGED
@@ -38,22 +38,88 @@ function loadProject(projectInput) {
38
38
  const packageJson = readJson(packagePath);
39
39
  const configPath = path.join(root, 'atom.config.json');
40
40
  const config = fs.existsSync(configPath) ? readJson(configPath) : {};
41
+ const build = objectOrEmpty(config.build);
42
+ const windows = objectOrEmpty(build.windows);
43
+ const macos = objectOrEmpty(build.macos);
44
+ const linux = objectOrEmpty(build.linux);
45
+ const dmg = objectOrEmpty(macos.dmg);
46
+ const productName = config.productName || packageJson.productName || packageJson.name || 'AtomJS App';
47
+ const icon = config.icon || null;
41
48
 
42
49
  return {
43
50
  root,
44
51
  packageJson,
45
52
  config: {
46
53
  appId: config.appId || `com.atomjs.${sanitizeId(packageJson.name || 'app')}`,
47
- productName: config.productName || packageJson.productName || packageJson.name || 'AtomJS App',
54
+ productName,
48
55
  main: config.main || packageJson.main || 'main.js',
49
- icon: config.icon || null,
56
+ icon,
50
57
  files: config.files || ['**/*'],
51
58
  installerCredit: config.installerCredit !== false,
59
+ build: {
60
+ artifactName: build.artifactName || '${productName}-${version}-${target}-${arch}',
61
+ windows: {
62
+ icon: windows.icon || icon,
63
+ installerIcon: windows.installerIcon || windows.icon || icon,
64
+ headerImage: windows.headerImage || null,
65
+ sidebarImage: windows.sidebarImage || null,
66
+ language: windows.language || 'English',
67
+ installMode: windows.installMode === 'machine' ? 'machine' : 'user',
68
+ installDirectory: windows.installDirectory || null,
69
+ createDesktopShortcut: windows.createDesktopShortcut !== false,
70
+ createStartMenuShortcut: windows.createStartMenuShortcut !== false,
71
+ allowDirectorySelection: windows.allowDirectorySelection !== false,
72
+ runAfterFinish: windows.runAfterFinish !== false,
73
+ welcomeText: windows.welcomeText || null,
74
+ finishText: windows.finishText || null,
75
+ publisher: windows.publisher || null,
76
+ requestedExecutionLevel: ['asInvoker', 'highestAvailable', 'requireAdministrator'].includes(windows.requestedExecutionLevel)
77
+ ? windows.requestedExecutionLevel
78
+ : 'asInvoker'
79
+ },
80
+ macos: {
81
+ icon: macos.icon || icon,
82
+ bundleName: macos.bundleName || productName,
83
+ category: macos.category || 'public.app-category.utilities',
84
+ minimumSystemVersion: macos.minimumSystemVersion || '12.0',
85
+ copyright: macos.copyright || null,
86
+ signingIdentity: macos.signingIdentity || '-',
87
+ entitlements: macos.entitlements || null,
88
+ hardenedRuntime: macos.hardenedRuntime === true,
89
+ dmg: {
90
+ enabled: dmg.enabled !== false,
91
+ artifactName: dmg.artifactName || null,
92
+ volumeName: dmg.volumeName || productName,
93
+ background: dmg.background || null
94
+ }
95
+ },
96
+ linux: {
97
+ icon: linux.icon || icon,
98
+ binaryName: linux.binaryName || sanitizeFilename(packageJson.name || productName).toLowerCase().replace(/\s+/g, '-'),
99
+ packageName: linux.packageName || sanitizeId(packageJson.name || productName),
100
+ category: linux.category || 'Utility',
101
+ maintainer: linux.maintainer || null,
102
+ description: linux.description || packageJson.description || productName,
103
+ dependencies: Array.isArray(linux.dependencies)
104
+ ? linux.dependencies.map(String)
105
+ : ['libgtk-3-0', 'libwebkit2gtk-4.1-0'],
106
+ rpmDependencies: Array.isArray(linux.rpmDependencies)
107
+ ? linux.rpmDependencies.map(String)
108
+ : ['gtk3', 'webkit2gtk4.1'],
109
+ appImage: linux.appImage !== false,
110
+ deb: linux.deb !== false,
111
+ rpm: linux.rpm !== false
112
+ }
113
+ },
52
114
  github: config.github || {}
53
115
  }
54
116
  };
55
117
  }
56
118
 
119
+ function objectOrEmpty(value) {
120
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
121
+ }
122
+
57
123
  function normalizeSpawn(command, args) {
58
124
  if (process.platform === 'win32' && /\.(?:cmd|bat)$/i.test(command)) {
59
125
  return { command: process.env.ComSpec || 'cmd.exe', args: ['/d', '/s', '/c', command, ...args] };