@gtkx/cli 2.0.0-beta.1 → 2.0.0-beta.3

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.
Files changed (238) hide show
  1. package/dist/builder.d.ts +1 -0
  2. package/dist/builder.d.ts.map +1 -1
  3. package/dist/builder.js +17 -5
  4. package/dist/builder.js.map +1 -1
  5. package/dist/cli.d.ts +1 -1
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +9 -1
  8. package/dist/cli.js.map +1 -1
  9. package/dist/codegen/reference.d.ts.map +1 -1
  10. package/dist/codegen/reference.js +1 -0
  11. package/dist/codegen/reference.js.map +1 -1
  12. package/dist/codegen/run-codegen.d.ts +4 -2
  13. package/dist/codegen/run-codegen.d.ts.map +1 -1
  14. package/dist/codegen/run-codegen.js +15 -16
  15. package/dist/codegen/run-codegen.js.map +1 -1
  16. package/dist/codegen/store-resolver.d.ts +2 -1
  17. package/dist/codegen/store-resolver.d.ts.map +1 -1
  18. package/dist/codegen/store-resolver.js +4 -3
  19. package/dist/codegen/store-resolver.js.map +1 -1
  20. package/dist/commands/build.d.ts +5 -0
  21. package/dist/commands/build.d.ts.map +1 -1
  22. package/dist/commands/build.js +83 -10
  23. package/dist/commands/build.js.map +1 -1
  24. package/dist/commands/codegen.d.ts +1 -0
  25. package/dist/commands/codegen.d.ts.map +1 -1
  26. package/dist/commands/codegen.js +6 -5
  27. package/dist/commands/codegen.js.map +1 -1
  28. package/dist/commands/deploy.d.ts +1 -0
  29. package/dist/commands/deploy.d.ts.map +1 -1
  30. package/dist/commands/deploy.js +4 -2
  31. package/dist/commands/deploy.js.map +1 -1
  32. package/dist/commands/dev.d.ts +9 -0
  33. package/dist/commands/dev.d.ts.map +1 -1
  34. package/dist/commands/dev.js +29 -4
  35. package/dist/commands/dev.js.map +1 -1
  36. package/dist/commands/docs.d.ts.map +1 -1
  37. package/dist/commands/docs.js +1 -0
  38. package/dist/commands/docs.js.map +1 -1
  39. package/dist/deploy/freedesktop/metainfo.d.ts.map +1 -1
  40. package/dist/deploy/freedesktop/metainfo.js +41 -2
  41. package/dist/deploy/freedesktop/metainfo.js.map +1 -1
  42. package/dist/deploy/freedesktop/validate.d.ts.map +1 -1
  43. package/dist/deploy/freedesktop/validate.js +10 -4
  44. package/dist/deploy/freedesktop/validate.js.map +1 -1
  45. package/dist/deploy/nfpm/config.d.ts +3 -2
  46. package/dist/deploy/nfpm/config.d.ts.map +1 -1
  47. package/dist/deploy/nfpm/config.js +7 -4
  48. package/dist/deploy/nfpm/config.js.map +1 -1
  49. package/dist/deploy/nfpm/contents.d.ts.map +1 -1
  50. package/dist/deploy/nfpm/contents.js +8 -0
  51. package/dist/deploy/nfpm/contents.js.map +1 -1
  52. package/dist/deploy/node-runtime/elf.d.ts +2 -1
  53. package/dist/deploy/node-runtime/elf.d.ts.map +1 -1
  54. package/dist/deploy/node-runtime/elf.js +32 -5
  55. package/dist/deploy/node-runtime/elf.js.map +1 -1
  56. package/dist/deploy/node-runtime/index.d.ts +2 -1
  57. package/dist/deploy/node-runtime/index.d.ts.map +1 -1
  58. package/dist/deploy/node-runtime/index.js +75 -5
  59. package/dist/deploy/node-runtime/index.js.map +1 -1
  60. package/dist/deploy/notices/node-runtime.js +2 -2
  61. package/dist/deploy/notices/node-runtime.js.map +1 -1
  62. package/dist/deploy/payload/launcher.d.ts.map +1 -1
  63. package/dist/deploy/payload/launcher.js +14 -1
  64. package/dist/deploy/payload/launcher.js.map +1 -1
  65. package/dist/deploy/payload/stage.js +1 -1
  66. package/dist/deploy/payload/stage.js.map +1 -1
  67. package/dist/deploy/run-deploy.d.ts +1 -0
  68. package/dist/deploy/run-deploy.d.ts.map +1 -1
  69. package/dist/deploy/run-deploy.js +100 -14
  70. package/dist/deploy/run-deploy.js.map +1 -1
  71. package/dist/deploy/settings/build-manifest.d.ts +6 -1
  72. package/dist/deploy/settings/build-manifest.d.ts.map +1 -1
  73. package/dist/deploy/settings/build-manifest.js +15 -4
  74. package/dist/deploy/settings/build-manifest.js.map +1 -1
  75. package/dist/deploy/settings/extra-files.d.ts +1 -1
  76. package/dist/deploy/settings/extra-files.d.ts.map +1 -1
  77. package/dist/deploy/settings/extra-files.js +22 -1
  78. package/dist/deploy/settings/extra-files.js.map +1 -1
  79. package/dist/deploy/settings/index.d.ts +1 -0
  80. package/dist/deploy/settings/index.d.ts.map +1 -1
  81. package/dist/deploy/settings/index.js +2 -1
  82. package/dist/deploy/settings/index.js.map +1 -1
  83. package/dist/deploy/settings/paths.d.ts +2 -1
  84. package/dist/deploy/settings/paths.d.ts.map +1 -1
  85. package/dist/deploy/settings/paths.js +113 -5
  86. package/dist/deploy/settings/paths.js.map +1 -1
  87. package/dist/deploy/targets/flatpak-source.d.ts.map +1 -1
  88. package/dist/deploy/targets/flatpak-source.js +11 -2
  89. package/dist/deploy/targets/flatpak-source.js.map +1 -1
  90. package/dist/deploy/targets/nfpm-target.d.ts.map +1 -1
  91. package/dist/deploy/targets/nfpm-target.js +12 -7
  92. package/dist/deploy/targets/nfpm-target.js.map +1 -1
  93. package/dist/deploy/types.d.ts +1 -0
  94. package/dist/deploy/types.d.ts.map +1 -1
  95. package/dist/deploy/types.js.map +1 -1
  96. package/dist/dev/entry-env.d.ts +2 -1
  97. package/dist/dev/entry-env.d.ts.map +1 -1
  98. package/dist/dev/entry-env.js +2 -1
  99. package/dist/dev/entry-env.js.map +1 -1
  100. package/dist/dev/headless.d.ts +3 -0
  101. package/dist/dev/headless.d.ts.map +1 -0
  102. package/dist/dev/headless.js +35 -0
  103. package/dist/dev/headless.js.map +1 -0
  104. package/dist/dev/runner-deps.d.ts +1 -1
  105. package/dist/dev/runner-deps.d.ts.map +1 -1
  106. package/dist/dev/runner-deps.js +3 -3
  107. package/dist/dev/runner-deps.js.map +1 -1
  108. package/dist/dev/runner-main.d.ts.map +1 -1
  109. package/dist/dev/runner-main.js +8 -3
  110. package/dist/dev/runner-main.js.map +1 -1
  111. package/dist/dev/supervisor.d.ts +1 -0
  112. package/dist/dev/supervisor.d.ts.map +1 -1
  113. package/dist/dev/supervisor.js +4 -3
  114. package/dist/dev/supervisor.js.map +1 -1
  115. package/dist/i18n/catalogs.d.ts.map +1 -1
  116. package/dist/i18n/catalogs.js +1 -0
  117. package/dist/i18n/catalogs.js.map +1 -1
  118. package/dist/i18n/source-messages.d.ts.map +1 -1
  119. package/dist/i18n/source-messages.js +960 -8
  120. package/dist/i18n/source-messages.js.map +1 -1
  121. package/dist/internal/build-manifest.d.ts +5 -2
  122. package/dist/internal/build-manifest.d.ts.map +1 -1
  123. package/dist/internal/build-manifest.js +15 -2
  124. package/dist/internal/build-manifest.js.map +1 -1
  125. package/dist/internal/build-output.d.ts +8 -0
  126. package/dist/internal/build-output.d.ts.map +1 -0
  127. package/dist/internal/build-output.js +79 -0
  128. package/dist/internal/build-output.js.map +1 -0
  129. package/dist/internal/docs-elements.d.ts +1 -0
  130. package/dist/internal/docs-elements.d.ts.map +1 -1
  131. package/dist/internal/docs-elements.js +5 -4
  132. package/dist/internal/docs-elements.js.map +1 -1
  133. package/dist/internal/entry-arg.d.ts +4 -1
  134. package/dist/internal/entry-arg.d.ts.map +1 -1
  135. package/dist/internal/entry-arg.js +7 -1
  136. package/dist/internal/entry-arg.js.map +1 -1
  137. package/dist/internal/errors.d.ts +2 -1
  138. package/dist/internal/errors.d.ts.map +1 -1
  139. package/dist/internal/errors.js +1 -1
  140. package/dist/internal/errors.js.map +1 -1
  141. package/dist/internal/output-directory.d.ts +15 -0
  142. package/dist/internal/output-directory.d.ts.map +1 -0
  143. package/dist/internal/output-directory.js +490 -0
  144. package/dist/internal/output-directory.js.map +1 -0
  145. package/dist/internal/parent-process.d.ts +12 -0
  146. package/dist/internal/parent-process.d.ts.map +1 -0
  147. package/dist/internal/parent-process.js +74 -0
  148. package/dist/internal/parent-process.js.map +1 -0
  149. package/dist/internal/prepare-project.d.ts +2 -0
  150. package/dist/internal/prepare-project.d.ts.map +1 -1
  151. package/dist/internal/prepare-project.js +3 -3
  152. package/dist/internal/prepare-project.js.map +1 -1
  153. package/dist/internal/source-imports.js +1 -1
  154. package/dist/internal/source-imports.js.map +1 -1
  155. package/dist/mcp/client.js +2 -2
  156. package/dist/mcp/client.js.map +1 -1
  157. package/dist/vite-plugins/build-manifest.d.ts +5 -1
  158. package/dist/vite-plugins/build-manifest.d.ts.map +1 -1
  159. package/dist/vite-plugins/build-manifest.js +2 -1
  160. package/dist/vite-plugins/build-manifest.js.map +1 -1
  161. package/dist/vite-plugins/ensure-store.d.ts +1 -1
  162. package/dist/vite-plugins/ensure-store.d.ts.map +1 -1
  163. package/dist/vite-plugins/ensure-store.js +2 -2
  164. package/dist/vite-plugins/ensure-store.js.map +1 -1
  165. package/dist/vite-plugins/i18n.d.ts.map +1 -1
  166. package/dist/vite-plugins/i18n.js +58 -6
  167. package/dist/vite-plugins/i18n.js.map +1 -1
  168. package/dist/vite-plugins/index.d.ts +8 -1
  169. package/dist/vite-plugins/index.d.ts.map +1 -1
  170. package/dist/vite-plugins/index.js +6 -2
  171. package/dist/vite-plugins/index.js.map +1 -1
  172. package/dist/vite-plugins/native.d.ts.map +1 -1
  173. package/dist/vite-plugins/native.js +0 -4
  174. package/dist/vite-plugins/native.js.map +1 -1
  175. package/dist/vite-plugins/self-contained.d.ts.map +1 -1
  176. package/dist/vite-plugins/self-contained.js +159 -158
  177. package/dist/vite-plugins/self-contained.js.map +1 -1
  178. package/dist/vite-plugins/settings-worker-env.d.ts +1 -1
  179. package/dist/vite-plugins/settings-worker-env.d.ts.map +1 -1
  180. package/dist/vite-plugins/settings-worker-env.js +2 -2
  181. package/dist/vite-plugins/settings-worker-env.js.map +1 -1
  182. package/dist/vitest-plugin.d.ts +2 -1
  183. package/dist/vitest-plugin.d.ts.map +1 -1
  184. package/dist/vitest-plugin.js +6 -6
  185. package/dist/vitest-plugin.js.map +1 -1
  186. package/package.json +10 -9
  187. package/src/builder.ts +18 -5
  188. package/src/cli.ts +9 -1
  189. package/src/codegen/reference.ts +1 -0
  190. package/src/codegen/run-codegen.ts +17 -19
  191. package/src/codegen/store-resolver.ts +5 -3
  192. package/src/commands/build.ts +13 -2
  193. package/src/commands/codegen.ts +6 -5
  194. package/src/commands/deploy.ts +4 -2
  195. package/src/commands/dev.ts +38 -4
  196. package/src/commands/docs.ts +1 -0
  197. package/src/deploy/freedesktop/metainfo.ts +73 -21
  198. package/src/deploy/freedesktop/validate.ts +12 -4
  199. package/src/deploy/nfpm/config.ts +13 -4
  200. package/src/deploy/nfpm/contents.ts +8 -0
  201. package/src/deploy/node-runtime/elf.ts +43 -5
  202. package/src/deploy/node-runtime/index.ts +107 -6
  203. package/src/deploy/notices/node-runtime.ts +2 -2
  204. package/src/deploy/payload/launcher.ts +20 -1
  205. package/src/deploy/payload/stage.ts +1 -1
  206. package/src/deploy/run-deploy.ts +56 -16
  207. package/src/deploy/settings/build-manifest.ts +28 -3
  208. package/src/deploy/settings/extra-files.ts +26 -2
  209. package/src/deploy/settings/index.ts +3 -1
  210. package/src/deploy/settings/paths.ts +70 -5
  211. package/src/deploy/targets/flatpak-source.ts +13 -2
  212. package/src/deploy/targets/nfpm-target.ts +13 -6
  213. package/src/deploy/types.ts +1 -0
  214. package/src/dev/entry-env.ts +2 -1
  215. package/src/dev/headless.ts +40 -0
  216. package/src/dev/runner-deps.ts +3 -3
  217. package/src/dev/runner-main.ts +10 -3
  218. package/src/dev/supervisor.ts +6 -3
  219. package/src/i18n/catalogs.ts +1 -0
  220. package/src/i18n/source-messages.ts +1429 -28
  221. package/src/internal/build-manifest.ts +25 -1
  222. package/src/internal/build-output.ts +113 -0
  223. package/src/internal/docs-elements.ts +10 -4
  224. package/src/internal/entry-arg.ts +8 -1
  225. package/src/internal/errors.ts +1 -1
  226. package/src/internal/output-directory.ts +699 -0
  227. package/src/internal/parent-process.ts +110 -0
  228. package/src/internal/prepare-project.ts +5 -5
  229. package/src/internal/source-imports.ts +1 -1
  230. package/src/mcp/client.ts +2 -2
  231. package/src/vite-plugins/build-manifest.ts +7 -1
  232. package/src/vite-plugins/ensure-store.ts +2 -2
  233. package/src/vite-plugins/i18n.ts +83 -5
  234. package/src/vite-plugins/index.ts +14 -7
  235. package/src/vite-plugins/native.ts +0 -5
  236. package/src/vite-plugins/self-contained.ts +184 -185
  237. package/src/vite-plugins/settings-worker-env.ts +2 -2
  238. package/src/vitest-plugin.ts +6 -6
@@ -14,6 +14,54 @@ import { element, renderDocument, text, type XmlNode } from "./xml.js";
14
14
 
15
15
  const COMPONENT_TYPE = "desktop-application";
16
16
  const DESKTOP_SUFFIX = ".desktop";
17
+ const COMPONENT_CLOSE = "</component>\n";
18
+ const PROVIDES_CLOSE = " </provides>\n";
19
+ const PROVIDES_FRAGMENT = /^<provides\s*>([\s\S]*)<\/provides>$/;
20
+
21
+ const normalizeExtraFragment = (fragment: string, index: number): string => {
22
+ const normalized = fragment.trim();
23
+
24
+ if (normalized.length === 0) {
25
+ throw new Error(`Cannot render deploy.metainfoExtra entry ${String(index + 1)}: it is empty`);
26
+ }
27
+
28
+ return normalized;
29
+ };
30
+
31
+ const indentFragment = (fragment: string, depth: number): string => {
32
+ const indentation = " ".repeat(depth * 4);
33
+
34
+ return fragment.split(/\r?\n/).map((line) => `${indentation}${line}`).join("\n");
35
+ };
36
+
37
+ const appendExtraFragments = (document: string, fragments: string[]): string => {
38
+ if (fragments.length === 0) {
39
+ return document;
40
+ }
41
+
42
+ let merged = document;
43
+ const componentFragments: string[] = [];
44
+
45
+ for (const [index, rawFragment] of fragments.entries()) {
46
+ const fragment = normalizeExtraFragment(rawFragment, index);
47
+ const provides = PROVIDES_FRAGMENT.exec(fragment)?.[1]?.trim();
48
+
49
+ if (provides === undefined) {
50
+ componentFragments.push(fragment);
51
+ continue;
52
+ }
53
+
54
+ merged = merged.replace(PROVIDES_CLOSE, () => `${indentFragment(provides, 2)}\n${PROVIDES_CLOSE}`);
55
+ }
56
+
57
+ if (componentFragments.length === 0) {
58
+ return merged;
59
+ }
60
+
61
+ const extra = componentFragments.map((fragment) => indentFragment(fragment, 1)).join("\n");
62
+
63
+ return merged.replace(COMPONENT_CLOSE, () => `${extra}\n${COMPONENT_CLOSE}`);
64
+ };
17
65
 
18
66
  const launchable = (applicationId: string): XmlNode => ({
19
67
  tag: "launchable",
@@ -21,26 +69,30 @@ const launchable = (applicationId: string): XmlNode => ({
21
69
  text: `${applicationId}${DESKTOP_SUFFIX}`,
22
70
  });
23
71
 
24
- const renderMetainfo = (settings: DeploySettings): string =>
25
- renderDocument(
26
- element("component", { type: COMPONENT_TYPE }, [
27
- text("id", settings.applicationId),
28
- text("name", settings.name),
29
- text("summary", settings.summary),
30
- text("metadata_license", settings.metadataLicense),
31
- text("project_license", settings.license),
32
- ...developerSection(settings),
33
- ...descriptionSection(settings),
34
- launchable(settings.applicationId),
35
- ...providesSection(settings),
36
- ...listSection("categories", "category", settings.categories),
37
- ...listSection("keywords", "keyword", settings.keywords),
38
- ...urlsSection(settings),
39
- ...screenshotsSection(settings),
40
- ...releasesSection(settings),
41
- ...contentRatingSection(settings),
42
- ...brandingSection(settings),
43
- ]),
44
- );
72
+ const metainfoRoot = (settings: DeploySettings): XmlNode =>
73
+ element("component", { type: COMPONENT_TYPE }, [
74
+ text("id", settings.applicationId),
75
+ text("name", settings.name),
76
+ text("summary", settings.summary),
77
+ text("metadata_license", settings.metadataLicense),
78
+ text("project_license", settings.license),
79
+ ...developerSection(settings),
80
+ ...descriptionSection(settings),
81
+ launchable(settings.applicationId),
82
+ ...providesSection(settings),
83
+ ...listSection("categories", "category", settings.categories),
84
+ ...listSection("keywords", "keyword", settings.keywords),
85
+ ...urlsSection(settings),
86
+ ...screenshotsSection(settings),
87
+ ...releasesSection(settings),
88
+ ...contentRatingSection(settings),
89
+ ...brandingSection(settings),
90
+ ]);
91
+
92
+ const renderMetainfo = (settings: DeploySettings): string => {
93
+ const document = renderDocument(metainfoRoot(settings));
94
+
95
+ return appendExtraFragments(document, settings.deploy.metainfoExtra ?? []);
96
+ };
45
97
 
46
98
  export { renderMetainfo };
@@ -12,6 +12,7 @@ type MetainfoResult = {
12
12
  status: number;
13
13
  errors: string[];
14
14
  warnings: string[];
15
+ rules: string[];
15
16
  areWarningsFatal: boolean;
16
17
  };
17
18
 
@@ -25,6 +26,7 @@ const DIAGNOSTIC = /^(?<severity>[EIPW]): \S+ (?<rule>[\w-]+)/;
25
26
  const ERROR_SEVERITY = "E";
26
27
  const WARNING_SEVERITY = "W";
27
28
  const INFO_SEVERITY = "I";
29
+ const FATAL_WARNING_RULES: Set<string> = new Set(["unknown-tag"]);
28
30
 
29
31
  const REMEDY_FOR_RULE: Record<string, string> = {
30
32
  "component-summary-missing": "set `deploy.summary`",
@@ -64,11 +66,15 @@ const remedyLines = (rules: string[]): string[] => {
64
66
  const invalid = (subject: string, output: string, rules: string[]): Error =>
65
67
  new Error([`${subject} is not valid:`, output.length > 0 ? output : "no output", ...remedyLines(rules)].join("\n"));
66
68
 
67
- const isFatalResult = ({ status, errors, warnings, areWarningsFatal }: MetainfoResult): boolean => {
69
+ const isFatalResult = ({ status, errors, warnings, rules, areWarningsFatal }: MetainfoResult): boolean => {
68
70
  if (errors.length > 0) {
69
71
  return true;
70
72
  }
71
73
 
74
+ if (rules.some((rule) => FATAL_WARNING_RULES.has(rule))) {
75
+ return true;
76
+ }
77
+
72
78
  if (areWarningsFatal && warnings.length > 0) {
73
79
  return true;
74
80
  }
@@ -78,7 +84,7 @@ const isFatalResult = ({ status, errors, warnings, areWarningsFatal }: MetainfoR
78
84
 
79
85
  const assertNotFatal = (result: MetainfoResult): void => {
80
86
  if (isFatalResult(result)) {
81
- throw invalid(result.subject, result.output, [...result.errors, ...result.warnings]);
87
+ throw invalid(result.subject, result.output, result.rules);
82
88
  }
83
89
  };
84
90
 
@@ -117,8 +123,10 @@ const validateMetainfo = (path: string, areWarningsFatal: boolean): void => {
117
123
 
118
124
  const errors = rulesIn(output, [ERROR_SEVERITY]);
119
125
  const warnings = rulesIn(output, [WARNING_SEVERITY]);
120
- assertNotFatal({ subject, output, status, errors, warnings, areWarningsFatal });
121
- reportDiagnostics(subject, [...warnings, ...rulesIn(output, [INFO_SEVERITY])]);
126
+ const infos = rulesIn(output, [INFO_SEVERITY]);
127
+ const rules = [...errors, ...warnings, ...infos];
128
+ assertNotFatal({ subject, output, status, errors, warnings, rules, areWarningsFatal });
129
+ reportDiagnostics(subject, [...warnings, ...infos]);
122
130
  };
123
131
 
124
132
  export { validateDesktopEntry, validateMetainfo };
@@ -1,5 +1,6 @@
1
1
  import type { DeployPayload, DeploySettings, StagedFile } from "../types.js";
2
2
  import { resolveDepends } from "../depends.js";
3
+ import { glibcMinimumForFiles } from "../node-runtime/elf.js";
3
4
  import { groupForCategories, sectionForCategories } from "../settings/categories.js";
4
5
  import { nfpmContents } from "./contents.js";
5
6
  import { debDescription, rpmDescription } from "./description.js";
@@ -113,6 +114,9 @@ const stagedFilesFor = (payload: DeployPayload, packager: NfpmPackager): StagedF
113
114
  ...payload.overlays[packager],
114
115
  ];
115
116
 
117
+ const glibcMinimumForPackage = (payload: DeployPayload, packager: NfpmPackager): string | null =>
118
+ glibcMinimumForFiles(stagedFilesFor(payload, packager).map((file) => file.abs));
119
+
116
120
  const packagerSettings = (settings: DeploySettings, packager: NfpmPackager): NfpmConfig =>
117
121
  packager === "rpm" ? rpmSettings(settings) : debSettings(settings);
118
122
 
@@ -122,10 +126,15 @@ const releaseFor = (settings: DeploySettings, packager: NfpmPackager): string =>
122
126
  const descriptionFor = (settings: DeploySettings, packager: NfpmPackager): string =>
123
127
  packager === "rpm" ? rpmDescription(settings) : debDescription(settings);
124
128
 
125
- const renderNfpmConfig = (payload: DeployPayload, packager: NfpmPackager): NfpmConfig => {
129
+ const renderNfpmConfig = (
130
+ payload: DeployPayload,
131
+ packager: NfpmPackager,
132
+ glibcMinimum: string | null,
133
+ ): NfpmConfig => {
126
134
  const settings = payload.settings;
127
135
  const isRpm = packager === "rpm";
128
- const depends = resolveDepends(settings, payload.node?.glibcMinimum ?? null);
136
+ const staged = stagedFilesFor(payload, packager);
137
+ const depends = resolveDepends(settings, glibcMinimum);
129
138
 
130
139
  return {
131
140
  name: packageNameFor(settings, packager),
@@ -146,8 +155,8 @@ const renderNfpmConfig = (payload: DeployPayload, packager: NfpmPackager): NfpmC
146
155
  ...relationsFor(settings, packager),
147
156
  ...optional("scripts", scriptsFor(settings)),
148
157
  [packager]: packagerSettings(settings, packager),
149
- contents: nfpmContents(PREFIX, stagedFilesFor(payload, packager), isRpm),
158
+ contents: nfpmContents(PREFIX, staged, isRpm),
150
159
  };
151
160
  };
152
161
 
153
- export { type NfpmPackager, packageNameFor, renderNfpmConfig };
162
+ export { glibcMinimumForPackage, type NfpmPackager, packageNameFor, renderNfpmConfig };
@@ -13,10 +13,16 @@ const FILESYSTEM_OWNED_PREFIXES = new Set([
13
13
  "/usr",
14
14
  "/usr/bin",
15
15
  "/usr/lib",
16
+ "/usr/lib/systemd",
17
+ "/usr/lib/systemd/system",
18
+ "/usr/lib/systemd/user",
19
+ "/usr/libexec",
16
20
  "/usr/share",
17
21
  "/usr/share/applications",
18
22
  "/usr/share/dbus-1",
19
23
  "/usr/share/dbus-1/services",
24
+ "/usr/share/dbus-1/system-services",
25
+ "/usr/share/dbus-1/system.d",
20
26
  "/usr/share/doc",
21
27
  "/usr/share/glib-2.0",
22
28
  "/usr/share/glib-2.0/schemas",
@@ -25,6 +31,8 @@ const FILESYSTEM_OWNED_PREFIXES = new Set([
25
31
  "/usr/share/metainfo",
26
32
  "/usr/share/mime",
27
33
  "/usr/share/mime/packages",
34
+ "/usr/share/polkit-1",
35
+ "/usr/share/polkit-1/actions",
28
36
  ]);
29
37
 
30
38
  const ICON_THEME_PREFIX = "/usr/share/icons/";
@@ -1,4 +1,4 @@
1
- import { readFileSync } from "node:fs";
1
+ import { closeSync, openSync, readFileSync, readSync } from "node:fs";
2
2
 
3
3
  type ElfSection = {
4
4
  name: string;
@@ -17,6 +17,7 @@ type DynamicEntry = {
17
17
  };
18
18
 
19
19
  const ELF_MAGIC = "ELF";
20
+ const ELF_MAGIC_SIZE = 4;
20
21
  const CLASS_64 = 2;
21
22
  const LITTLE_ENDIAN = 1;
22
23
  const OFFSET_CLASS = 4;
@@ -35,8 +36,11 @@ const DYNAMIC_VALUE_OFFSET = 8;
35
36
  const GLIBC_VERSION = /GLIBC_(\d+)\.(\d+)/g;
36
37
  const VERSION_RADIX = 1000;
37
38
 
39
+ const hasElfMagic = (buffer: Buffer): boolean =>
40
+ buffer.length >= OFFSET_CLASS && buffer.subarray(0, 4).toString("latin1") === ELF_MAGIC;
41
+
38
42
  const assertElf64 = (buffer: Buffer, path: string): void => {
39
- if (buffer.length < OFFSET_SECTION_NAME_INDEX || buffer.subarray(0, 4).toString("latin1") !== ELF_MAGIC) {
43
+ if (buffer.length < OFFSET_SECTION_NAME_INDEX || !hasElfMagic(buffer)) {
40
44
  throw new Error(`Cannot read ${path}: it is not an ELF binary`);
41
45
  }
42
46
 
@@ -130,8 +134,7 @@ const readGlibcMinimum = (strings: Buffer): string | null => {
130
134
  const stringTable = (buffer: Buffer, dynstr: ElfSection | undefined): Buffer =>
131
135
  dynstr === undefined ? Buffer.alloc(0) : buffer.subarray(dynstr.offset, dynstr.offset + dynstr.size);
132
136
 
133
- const readElfInfo = (path: string): ElfInfo => {
134
- const buffer = readFileSync(path);
137
+ const readElfBuffer = (buffer: Buffer, path: string): ElfInfo => {
135
138
  assertElf64(buffer, path);
136
139
  const sections = readSections(buffer);
137
140
  const strings = stringTable(buffer, sectionFor(sections, ".dynstr"));
@@ -142,4 +145,39 @@ const readElfInfo = (path: string): ElfInfo => {
142
145
  };
143
146
  };
144
147
 
145
- export { type ElfInfo, readElfInfo };
148
+ const readElfInfo = (path: string): ElfInfo => readElfBuffer(readFileSync(path), path);
149
+
150
+ const readFilePrefix = (descriptor: number, size: number): Buffer => {
151
+ const buffer = Buffer.alloc(size);
152
+ const bytesRead = readSync(descriptor, buffer, 0, size, 0);
153
+
154
+ return buffer.subarray(0, bytesRead);
155
+ };
156
+
157
+ const readOptionalElfInfo = (path: string): ElfInfo | null => {
158
+ const descriptor = openSync(path, "r");
159
+
160
+ try {
161
+ const prefix = readFilePrefix(descriptor, ELF_MAGIC_SIZE);
162
+
163
+ return hasElfMagic(prefix) ? readElfBuffer(readFileSync(descriptor), path) : null;
164
+ } finally {
165
+ closeSync(descriptor);
166
+ }
167
+ };
168
+
169
+ const glibcMinimumForFiles = (paths: Iterable<string>): string | null => {
170
+ const versions: number[][] = [];
171
+
172
+ for (const path of paths) {
173
+ const minimum = readOptionalElfInfo(path)?.glibcMinimum;
174
+
175
+ if (minimum !== undefined && minimum !== null) {
176
+ versions.push(minimum.split(".").map(Number));
177
+ }
178
+ }
179
+
180
+ return formatVersion(highestVersion(versions));
181
+ };
182
+
183
+ export { type ElfInfo, glibcMinimumForFiles, readElfInfo };
@@ -1,4 +1,6 @@
1
+ import { MINIMUM_NODE_VERSION } from "@gtkx/config/internal";
1
2
  import { info, tryResolveExecutable } from "@gtkx/utils";
3
+ import { execFileSync } from "node:child_process";
2
4
  import { chmodSync, copyFileSync, mkdirSync, statSync } from "node:fs";
3
5
  import { join, resolve } from "node:path";
4
6
  import type { DeploySettings, NodeRuntime } from "../types.js";
@@ -13,13 +15,92 @@ type StagedRuntime = {
13
15
  licenseFile: string | null;
14
16
  };
15
17
 
18
+ type NodeSource = "download" | "host" | "path";
19
+
16
20
  const BYTES_PER_MIB = 1024 * 1024;
17
21
  const EXECUTABLE_MODE = 0o755;
18
22
  const NODE_FILENAME = "node";
23
+ const NODE_VERSION_PATTERN = /^v?(\d+)\.(\d+)\.(\d+)$/;
24
+ const VERSION_PROBE_TIMEOUT_MS = 10_000;
25
+ const VERSION_PROBE_MAX_BUFFER = 1024;
26
+
27
+ const nodeSourceFor = (settings: DeploySettings): NodeSource => settings.deploy.node?.source ?? "download";
28
+
29
+ const parseNodeVersion = (value: string, subject: string): { parts: number[]; version: string } => {
30
+ const match = NODE_VERSION_PATTERN.exec(value.trim());
31
+
32
+ if (match === null) {
33
+ throw new Error(`Cannot determine the Node.js version for ${subject}: expected a version such as 26.7.0`);
34
+ }
35
+
36
+ const parts = match.slice(1).map(Number);
37
+
38
+ return { parts, version: parts.map(String).join(".") };
39
+ };
40
+
41
+ const compareVersions = (left: number[], right: number[]): number => {
42
+ for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
43
+ const difference = (left[index] ?? 0) - (right[index] ?? 0);
44
+
45
+ if (difference !== 0) {
46
+ return difference;
47
+ }
48
+ }
49
+
50
+ return 0;
51
+ };
52
+
53
+ const supportedNodeVersion = (value: string, subject: string): string => {
54
+ const parsed = parseNodeVersion(value, subject);
55
+ const minimum = parseNodeVersion(MINIMUM_NODE_VERSION, "GTKX");
56
+
57
+ if (compareVersions(parsed.parts, minimum.parts) < 0) {
58
+ throw new Error(
59
+ `Cannot bundle Node.js ${parsed.version} for ${subject}: GTKX requires ${MINIMUM_NODE_VERSION} or newer`,
60
+ );
61
+ }
62
+
63
+ return parsed.version;
64
+ };
65
+
66
+ const probeNodeVersion = (path: string): string => {
67
+ try {
68
+ const output = execFileSync(path, ["--version"], {
69
+ encoding: "utf8",
70
+ env: { ...process.env, NODE_OPTIONS: "" },
71
+ maxBuffer: VERSION_PROBE_MAX_BUFFER,
72
+ timeout: VERSION_PROBE_TIMEOUT_MS,
73
+ });
74
+
75
+ return supportedNodeVersion(output, path);
76
+ } catch (error) {
77
+ if (error instanceof Error && error.message.startsWith("Cannot bundle Node.js")) {
78
+ throw error;
79
+ }
80
+
81
+ throw new Error(`Cannot determine the Node.js version from ${path}`, { cause: error });
82
+ }
83
+ };
84
+
85
+ const assertExpectedVersion = (configured: string | undefined, actual: string, source: NodeSource): string => {
86
+ if (configured === undefined) {
87
+ return actual;
88
+ }
89
+
90
+ const expected = parseNodeVersion(configured, `deploy.node.source: "${source}"`).version;
91
+
92
+ if (expected !== actual) {
93
+ throw new Error(
94
+ `The Node.js runtime from deploy.node.source "${source}" is ${actual}, not the configured ${expected}`,
95
+ );
96
+ }
97
+
98
+ return actual;
99
+ };
19
100
 
20
101
  const sourcePathFor = (settings: DeploySettings): string => {
21
102
  const node = settings.deploy.node ?? {};
22
- const source = node.source ?? "download";
103
+ const source = nodeSourceFor(settings);
23
104
 
24
105
  if (source === "host") {
25
106
  return process.execPath;
@@ -32,6 +113,21 @@ const sourcePathFor = (settings: DeploySettings): string => {
32
113
  return resolve(settings.paths.root, node.path);
33
114
  };
34
115
 
116
+ const resolveNodeVersion = (settings: DeploySettings): string => {
117
+ const node = settings.deploy.node ?? {};
118
+ const source = nodeSourceFor(settings);
119
+
120
+ if (source === "download") {
121
+ return supportedNodeVersion(node.version ?? MINIMUM_NODE_VERSION, 'deploy.node.source: "download"');
122
+ }
123
+
124
+ const actual = source === "host"
125
+ ? supportedNodeVersion(process.versions.node, 'deploy.node.source: "host"')
126
+ : probeNodeVersion(sourcePathFor(settings));
127
+
128
+ return assertExpectedVersion(node.version, actual, source);
129
+ };
130
+
35
131
  const didStripBinary = (path: string): boolean => {
36
132
  if (tryResolveExecutable("strip") === undefined) {
37
133
  return false;
@@ -60,7 +156,11 @@ const stageDownloadedNode = async (settings: DeploySettings, version: string): P
60
156
  return downloaded;
61
157
  };
62
158
 
63
- const stageFromSource = async (settings: DeploySettings, version: string, source: string): Promise<StagedRuntime> => {
159
+ const stageFromSource = async (
160
+ settings: DeploySettings,
161
+ version: string,
162
+ source: NodeSource,
163
+ ): Promise<StagedRuntime> => {
64
164
  if (source === "download") {
65
165
  return stageDownloadedNode(settings, version);
66
166
  }
@@ -73,14 +173,15 @@ const stageFromSource = async (settings: DeploySettings, version: string, source
73
173
 
74
174
  const resolveNodeRuntime = async (settings: DeploySettings): Promise<NodeRuntime> => {
75
175
  const node = settings.deploy.node ?? {};
76
- const source = node.source ?? "download";
77
- const version = node.version ?? process.versions.node;
176
+ const source = nodeSourceFor(settings);
177
+ const version = resolveNodeVersion(settings);
78
178
  const staged = await stageFromSource(settings, version, source);
79
179
  const isStripped = node.shouldStrip === false ? false : didStripBinary(staged.path);
80
180
  const elf = readElfInfo(staged.path);
81
- info(`Bundled Node.js v${version} (${megabytes(staged.path)} MiB, glibc >= ${elf.glibcMinimum ?? "unknown"})`);
181
+ const glibcMinimum = elf.glibcMinimum ?? "unknown";
182
+ info(`Bundled Node.js v${version} (${megabytes(staged.path)} MiB, runtime glibc >= ${glibcMinimum})`);
82
183
 
83
184
  return { ...staged, version, glibcMinimum: elf.glibcMinimum, isStripped };
84
185
  };
85
186
 
86
- export { resolveNodeRuntime, sourcePathFor };
187
+ export { resolveNodeRuntime, resolveNodeVersion, sourcePathFor };
@@ -1,6 +1,6 @@
1
1
  import { warn } from "@gtkx/utils";
2
2
  import type { DeploySettings, NodeRuntime, NoticeSection } from "../types.js";
3
- import { sourcePathFor } from "../node-runtime/index.js";
3
+ import { resolveNodeVersion, sourcePathFor } from "../node-runtime/index.js";
4
4
  import { licenseBesideNode } from "../node-runtime/license.js";
5
5
  import { NODE_FILENAME } from "../payload/launcher.js";
6
6
  import { nodeLicenseDestination } from "../payload/stage.js";
@@ -32,7 +32,7 @@ const MISSING_LICENSE =
32
32
  const isSourceMode = (settings: DeploySettings): boolean => settings.deploy.flatpak?.mode === "source";
33
33
 
34
34
  const versionFor = (settings: DeploySettings, node: NodeRuntime | null): string =>
35
- node?.version ?? settings.deploy.node?.version ?? process.versions.node;
35
+ node?.version ?? resolveNodeVersion(settings);
36
36
 
37
37
  const licenseFileFor = (settings: DeploySettings, node: NodeRuntime | null): string | null => {
38
38
  if (node !== null) {
@@ -2,6 +2,8 @@ import type { DeploySettings } from "../types.js";
2
2
  import { BUNDLE_FILENAME } from "../../vite-plugins/esm-extension.js";
3
3
 
4
4
  const NODE_FILENAME = "node";
5
+ const SHELL_SINGLE_QUOTE = "'";
6
+ const SHELL_SINGLE_QUOTE_ESCAPE = "'\"'\"'";
5
7
 
6
8
  const compileCacheLines = (binaryName: string): string[] => [
7
9
  "cache_home=${XDG_CACHE_HOME:-${HOME:-/tmp}/.cache}",
@@ -9,6 +11,22 @@ const compileCacheLines = (binaryName: string): string[] => [
9
11
  "export NODE_COMPILE_CACHE",
10
12
  ];
11
13
 
14
+ const shellQuote = (value: string): string =>
15
+ SHELL_SINGLE_QUOTE + value.split(SHELL_SINGLE_QUOTE).join(SHELL_SINGLE_QUOTE_ESCAPE) + SHELL_SINGLE_QUOTE;
16
+
17
+ const launcherEnvLines = (settings: DeploySettings): string[] =>
18
+ Object.entries(settings.deploy.launcherEnv ?? {})
19
+ .toSorted(([left], [right]) => left.localeCompare(right))
20
+ .map(([name, value]) => `export ${name}=${shellQuote(value)}`);
21
+
22
+ const launcherCommand = (settings: DeploySettings, libDir: string): string =>
23
+ [
24
+ `"${libDir}/${NODE_FILENAME}"`,
25
+ ...(settings.deploy.nodeFlags ?? []).map((flag) => shellQuote(flag)),
26
+ `"${libDir}/${BUNDLE_FILENAME}"`,
27
+ '"$@"',
28
+ ].join(" ");
29
+
12
30
  const renderLauncher = (settings: DeploySettings): string => {
13
31
  const libDir = `$prefix/lib/${settings.binaryName}`;
14
32
  const shouldUseCompileCache = settings.deploy.node?.shouldUseCompileCache !== false;
@@ -21,7 +39,8 @@ const renderLauncher = (settings: DeploySettings): string => {
21
39
  'GTKX_LOCALE_DIR="$prefix/share/locale"',
22
40
  "export GTKX_LOCALE_DIR",
23
41
  ...(shouldUseCompileCache ? compileCacheLines(settings.binaryName) : []),
24
- `exec "${libDir}/${NODE_FILENAME}" "${libDir}/${BUNDLE_FILENAME}" "$@"`,
42
+ ...launcherEnvLines(settings),
43
+ `exec ${launcherCommand(settings, libDir)}`,
25
44
  "",
26
45
  ].join("\n");
27
46
  };
@@ -108,10 +108,10 @@ const stagePayload = ({ settings, node, metadata }: StageRequest): StagedFile[]
108
108
  ...stageNodeBinary(settings, root, node),
109
109
  ...stageRuntimeFiles(settings, root),
110
110
  ...stageCatalogs(settings, root),
111
+ ...stageExtraFiles(settings, root),
111
112
  ...stageMetadata(settings, root, metadata),
112
113
  ...stageIcons(settings, root),
113
114
  ...stageSchemas(settings, root),
114
- ...stageExtraFiles(settings, root),
115
115
  ]);
116
116
  };
117
117