@gtkx/cli 2.0.0-beta.6 → 2.0.0-beta.8

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 (205) hide show
  1. package/README.md +43 -40
  2. package/dist/cli.js +1 -1
  3. package/dist/cli.js.map +1 -1
  4. package/dist/codegen/freshness.d.ts.map +1 -1
  5. package/dist/codegen/freshness.js +5 -1
  6. package/dist/codegen/freshness.js.map +1 -1
  7. package/dist/commands/deploy.d.ts +4 -0
  8. package/dist/commands/deploy.d.ts.map +1 -1
  9. package/dist/commands/deploy.js +6 -0
  10. package/dist/commands/deploy.js.map +1 -1
  11. package/dist/deploy/download.d.ts +7 -3
  12. package/dist/deploy/download.d.ts.map +1 -1
  13. package/dist/deploy/download.js +19 -15
  14. package/dist/deploy/download.js.map +1 -1
  15. package/dist/deploy/native-addon.d.ts +5 -0
  16. package/dist/deploy/native-addon.d.ts.map +1 -0
  17. package/dist/deploy/native-addon.js +134 -0
  18. package/dist/deploy/native-addon.js.map +1 -0
  19. package/dist/deploy/node-runtime/download.js +2 -2
  20. package/dist/deploy/node-runtime/download.js.map +1 -1
  21. package/dist/deploy/node-runtime/elf.d.ts +3 -1
  22. package/dist/deploy/node-runtime/elf.d.ts.map +1 -1
  23. package/dist/deploy/node-runtime/elf.js +3 -1
  24. package/dist/deploy/node-runtime/elf.js.map +1 -1
  25. package/dist/deploy/node-runtime/index.d.ts.map +1 -1
  26. package/dist/deploy/node-runtime/index.js +14 -2
  27. package/dist/deploy/node-runtime/index.js.map +1 -1
  28. package/dist/deploy/payload/stage.d.ts +2 -1
  29. package/dist/deploy/payload/stage.d.ts.map +1 -1
  30. package/dist/deploy/payload/stage.js +22 -2
  31. package/dist/deploy/payload/stage.js.map +1 -1
  32. package/dist/deploy/run-deploy.d.ts +1 -0
  33. package/dist/deploy/run-deploy.d.ts.map +1 -1
  34. package/dist/deploy/run-deploy.js +135 -79
  35. package/dist/deploy/run-deploy.js.map +1 -1
  36. package/dist/deploy/settings/arch.d.ts +9 -3
  37. package/dist/deploy/settings/arch.d.ts.map +1 -1
  38. package/dist/deploy/settings/arch.js +27 -6
  39. package/dist/deploy/settings/arch.js.map +1 -1
  40. package/dist/deploy/settings/index.d.ts +2 -1
  41. package/dist/deploy/settings/index.d.ts.map +1 -1
  42. package/dist/deploy/settings/index.js +2 -1
  43. package/dist/deploy/settings/index.js.map +1 -1
  44. package/dist/deploy/settings/libraries.d.ts.map +1 -1
  45. package/dist/deploy/settings/libraries.js +2 -1
  46. package/dist/deploy/settings/libraries.js.map +1 -1
  47. package/dist/deploy/settings/paths.d.ts +2 -1
  48. package/dist/deploy/settings/paths.d.ts.map +1 -1
  49. package/dist/deploy/settings/paths.js +6 -5
  50. package/dist/deploy/settings/paths.js.map +1 -1
  51. package/dist/deploy/targets/appimage.d.ts.map +1 -1
  52. package/dist/deploy/targets/appimage.js +1 -0
  53. package/dist/deploy/targets/appimage.js.map +1 -1
  54. package/dist/deploy/targets/flatpak-pnpm.js +4 -4
  55. package/dist/deploy/targets/flatpak-pnpm.js.map +1 -1
  56. package/dist/deploy/targets/flatpak-source.d.ts.map +1 -1
  57. package/dist/deploy/targets/flatpak-source.js +2 -0
  58. package/dist/deploy/targets/flatpak-source.js.map +1 -1
  59. package/dist/deploy/targets/flatpak.d.ts.map +1 -1
  60. package/dist/deploy/targets/flatpak.js +1 -0
  61. package/dist/deploy/targets/flatpak.js.map +1 -1
  62. package/dist/deploy/targets/nfpm-target.d.ts.map +1 -1
  63. package/dist/deploy/targets/nfpm-target.js +1 -0
  64. package/dist/deploy/targets/nfpm-target.js.map +1 -1
  65. package/dist/deploy/types.d.ts +4 -2
  66. package/dist/deploy/types.d.ts.map +1 -1
  67. package/dist/deploy/types.js.map +1 -1
  68. package/dist/deploy/vendored/appimagetool.js +2 -2
  69. package/dist/deploy/vendored/appimagetool.js.map +1 -1
  70. package/dist/deploy/vendored/nfpm.js +1 -1
  71. package/dist/deploy/vendored/nfpm.js.map +1 -1
  72. package/dist/dev/font-dir.d.ts +3 -0
  73. package/dist/dev/font-dir.d.ts.map +1 -0
  74. package/dist/dev/font-dir.js +9 -0
  75. package/dist/dev/font-dir.js.map +1 -0
  76. package/dist/dev/runner-main.d.ts.map +1 -1
  77. package/dist/dev/runner-main.js +2 -0
  78. package/dist/dev/runner-main.js.map +1 -1
  79. package/dist/dev/runner.d.ts.map +1 -1
  80. package/dist/dev/runner.js +19 -6
  81. package/dist/dev/runner.js.map +1 -1
  82. package/dist/internal/agent-rules.d.ts.map +1 -1
  83. package/dist/internal/agent-rules.js +6 -2
  84. package/dist/internal/agent-rules.js.map +1 -1
  85. package/dist/internal/banner.d.ts.map +1 -1
  86. package/dist/internal/banner.js +3 -0
  87. package/dist/internal/banner.js.map +1 -1
  88. package/dist/internal/font-path.d.ts +4 -0
  89. package/dist/internal/font-path.d.ts.map +1 -0
  90. package/dist/internal/font-path.js +12 -0
  91. package/dist/internal/font-path.js.map +1 -0
  92. package/dist/internal/font-staging.d.ts +6 -0
  93. package/dist/internal/font-staging.d.ts.map +1 -0
  94. package/dist/internal/font-staging.js +92 -0
  95. package/dist/internal/font-staging.js.map +1 -0
  96. package/dist/internal/source-imports.d.ts +2 -1
  97. package/dist/internal/source-imports.d.ts.map +1 -1
  98. package/dist/internal/source-imports.js +8 -3
  99. package/dist/internal/source-imports.js.map +1 -1
  100. package/dist/internal/xdg-banner.d.ts +4 -0
  101. package/dist/internal/xdg-banner.d.ts.map +1 -0
  102. package/dist/internal/xdg-banner.js +11 -0
  103. package/dist/internal/xdg-banner.js.map +1 -0
  104. package/dist/internal/xdg-data-dirs.d.ts +2 -1
  105. package/dist/internal/xdg-data-dirs.d.ts.map +1 -1
  106. package/dist/internal/xdg-data-dirs.js +1 -1
  107. package/dist/internal/xdg-data-dirs.js.map +1 -1
  108. package/dist/settings/schema.d.ts.map +1 -1
  109. package/dist/settings/schema.js +1 -6
  110. package/dist/settings/schema.js.map +1 -1
  111. package/dist/vite-plugins/asset-extensions.d.ts.map +1 -1
  112. package/dist/vite-plugins/asset-extensions.js +2 -0
  113. package/dist/vite-plugins/asset-extensions.js.map +1 -1
  114. package/dist/vite-plugins/asset-imports.d.ts.map +1 -1
  115. package/dist/vite-plugins/asset-imports.js +7 -2
  116. package/dist/vite-plugins/asset-imports.js.map +1 -1
  117. package/dist/vite-plugins/asset-specifier.d.ts +5 -1
  118. package/dist/vite-plugins/asset-specifier.d.ts.map +1 -1
  119. package/dist/vite-plugins/asset-specifier.js +10 -1
  120. package/dist/vite-plugins/asset-specifier.js.map +1 -1
  121. package/dist/vite-plugins/css.d.ts.map +1 -1
  122. package/dist/vite-plugins/css.js +3 -2
  123. package/dist/vite-plugins/css.js.map +1 -1
  124. package/dist/vite-plugins/font-name.d.ts +3 -0
  125. package/dist/vite-plugins/font-name.d.ts.map +1 -0
  126. package/dist/vite-plugins/font-name.js +406 -0
  127. package/dist/vite-plugins/font-name.js.map +1 -0
  128. package/dist/vite-plugins/font-worker-env.d.ts +4 -0
  129. package/dist/vite-plugins/font-worker-env.d.ts.map +1 -0
  130. package/dist/vite-plugins/font-worker-env.js +19 -0
  131. package/dist/vite-plugins/font-worker-env.js.map +1 -0
  132. package/dist/vite-plugins/font.d.ts +4 -0
  133. package/dist/vite-plugins/font.d.ts.map +1 -0
  134. package/dist/vite-plugins/font.js +99 -0
  135. package/dist/vite-plugins/font.js.map +1 -0
  136. package/dist/vite-plugins/icons.d.ts.map +1 -1
  137. package/dist/vite-plugins/icons.js +5 -10
  138. package/dist/vite-plugins/icons.js.map +1 -1
  139. package/dist/vite-plugins/index.d.ts.map +1 -1
  140. package/dist/vite-plugins/index.js +2 -0
  141. package/dist/vite-plugins/index.js.map +1 -1
  142. package/dist/vite-plugins/react-compiler.js +2 -2
  143. package/dist/vite-plugins/react-compiler.js.map +1 -1
  144. package/dist/vite-plugins/resources.js +3 -3
  145. package/dist/vite-plugins/resources.js.map +1 -1
  146. package/dist/vite-plugins/settings-worker-env.js +2 -2
  147. package/dist/vite-plugins/settings-worker-env.js.map +1 -1
  148. package/dist/vite-plugins/store-links.d.ts.map +1 -1
  149. package/dist/vite-plugins/store-links.js +2 -1
  150. package/dist/vite-plugins/store-links.js.map +1 -1
  151. package/dist/vite-plugins/undeclared-library.js +5 -5
  152. package/dist/vite-plugins/undeclared-library.js.map +1 -1
  153. package/dist/vitest-plugin.d.ts.map +1 -1
  154. package/dist/vitest-plugin.js +2 -0
  155. package/dist/vitest-plugin.js.map +1 -1
  156. package/env.d.ts +9 -0
  157. package/package.json +17 -15
  158. package/src/cli.ts +1 -1
  159. package/src/codegen/freshness.ts +5 -1
  160. package/src/commands/deploy.ts +6 -0
  161. package/src/deploy/download.ts +33 -17
  162. package/src/deploy/native-addon.ts +183 -0
  163. package/src/deploy/node-runtime/download.ts +2 -2
  164. package/src/deploy/node-runtime/elf.ts +4 -1
  165. package/src/deploy/node-runtime/index.ts +24 -4
  166. package/src/deploy/payload/stage.ts +34 -2
  167. package/src/deploy/run-deploy.ts +203 -93
  168. package/src/deploy/settings/arch.ts +40 -8
  169. package/src/deploy/settings/index.ts +4 -1
  170. package/src/deploy/settings/libraries.ts +2 -1
  171. package/src/deploy/settings/paths.ts +8 -6
  172. package/src/deploy/targets/appimage.ts +1 -0
  173. package/src/deploy/targets/flatpak-pnpm.ts +4 -4
  174. package/src/deploy/targets/flatpak-source.ts +2 -0
  175. package/src/deploy/targets/flatpak.ts +1 -0
  176. package/src/deploy/targets/nfpm-target.ts +1 -0
  177. package/src/deploy/types.ts +4 -1
  178. package/src/deploy/vendored/appimagetool.ts +2 -2
  179. package/src/deploy/vendored/nfpm.ts +1 -1
  180. package/src/dev/font-dir.ts +11 -0
  181. package/src/dev/runner-main.ts +2 -0
  182. package/src/dev/runner.ts +24 -6
  183. package/src/internal/agent-rules.ts +6 -2
  184. package/src/internal/banner.ts +4 -0
  185. package/src/internal/font-path.ts +15 -0
  186. package/src/internal/font-staging.ts +129 -0
  187. package/src/internal/source-imports.ts +18 -3
  188. package/src/internal/xdg-banner.ts +13 -0
  189. package/src/internal/xdg-data-dirs.ts +1 -1
  190. package/src/settings/schema.ts +1 -8
  191. package/src/vite-plugins/asset-extensions.ts +2 -0
  192. package/src/vite-plugins/asset-imports.ts +9 -1
  193. package/src/vite-plugins/asset-specifier.ts +17 -0
  194. package/src/vite-plugins/css.ts +4 -2
  195. package/src/vite-plugins/font-name.ts +582 -0
  196. package/src/vite-plugins/font-worker-env.ts +25 -0
  197. package/src/vite-plugins/font.ts +165 -0
  198. package/src/vite-plugins/icons.ts +6 -12
  199. package/src/vite-plugins/index.ts +2 -0
  200. package/src/vite-plugins/react-compiler.ts +2 -2
  201. package/src/vite-plugins/resources.ts +3 -3
  202. package/src/vite-plugins/settings-worker-env.ts +2 -2
  203. package/src/vite-plugins/store-links.ts +2 -1
  204. package/src/vite-plugins/undeclared-library.ts +5 -5
  205. package/src/vitest-plugin.ts +2 -0
@@ -1,5 +1,6 @@
1
1
  import gtkxVitest, {} from "@gtkx/vitest";
2
2
  import { gtkxEnsureStore } from "./vite-plugins/ensure-store.js";
3
+ import { gtkxFontWorkerEnv } from "./vite-plugins/font-worker-env.js";
3
4
  import { gtkxVitePlugins } from "./vite-plugins/index.js";
4
5
  import { gtkxSettingsWorkerEnv } from "./vite-plugins/settings-worker-env.js";
5
6
  /**
@@ -13,6 +14,7 @@ const gtkx = (options = {}) => [
13
14
  gtkxEnsureStore(options.configFile),
14
15
  ...gtkxVitePlugins({ configFile: options.configFile }),
15
16
  gtkxSettingsWorkerEnv(options.configFile),
17
+ gtkxFontWorkerEnv(options.configFile),
16
18
  gtkxVitest(options),
17
19
  ];
18
20
  export default gtkx;
@@ -1 +1 @@
1
- {"version":3,"file":"vitest-plugin.js","sourceRoot":"","sources":["../src/vitest-plugin.ts"],"names":[],"mappings":"AACA,OAAO,UAAU,EAAE,EAAsB,MAAM,cAAc,CAAC;AAC9D,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AACjE,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AAE9E;;;;;;GAMG;AACH,MAAM,IAAI,GAAG,CAAC,UAAyB,EAAE,EAAY,EAAE,CAAC;IACpD,eAAe,CAAC,OAAO,CAAC,UAAU,CAAC;IACnC,GAAG,eAAe,CAAC,EAAE,UAAU,EAAE,OAAO,CAAC,UAAU,EAAE,CAAC;IACtD,qBAAqB,CAAC,OAAO,CAAC,UAAU,CAAC;IACzC,UAAU,CAAC,OAAO,CAAC;CACtB,CAAC;AAEF,eAAe,IAAI,CAAC","sourcesContent":["import type { Plugin } from \"vite\";\nimport gtkxVitest, { type PluginOptions } from \"@gtkx/vitest\";\nimport { gtkxEnsureStore } from \"./vite-plugins/ensure-store.js\";\nimport { gtkxVitePlugins } from \"./vite-plugins/index.js\";\nimport { gtkxSettingsWorkerEnv } from \"./vite-plugins/settings-worker-env.js\";\n\n/**\n * Vite plugin stack for testing a GTKX application: the generated bindings the tests import,\n * the plugins the app is built with, the project's compiled GSettings schemas exposed to every\n * test worker, and a headless display per worker.\n *\n * @returns The plugins to spread into a Vitest config.\n */\nconst gtkx = (options: PluginOptions = {}): Plugin[] => [\n gtkxEnsureStore(options.configFile),\n ...gtkxVitePlugins({ configFile: options.configFile }),\n gtkxSettingsWorkerEnv(options.configFile),\n gtkxVitest(options),\n];\n\nexport default gtkx;\n"]}
1
+ {"version":3,"file":"vitest-plugin.js","sourceRoot":"","sources":["../src/vitest-plugin.ts"],"names":[],"mappings":"AACA,OAAO,UAAU,EAAE,EAAsB,MAAM,cAAc,CAAC;AAC9D,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AACjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AACtE,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AAE9E;;;;;;GAMG;AACH,MAAM,IAAI,GAAG,CAAC,UAAyB,EAAE,EAAY,EAAE,CAAC;IACpD,eAAe,CAAC,OAAO,CAAC,UAAU,CAAC;IACnC,GAAG,eAAe,CAAC,EAAE,UAAU,EAAE,OAAO,CAAC,UAAU,EAAE,CAAC;IACtD,qBAAqB,CAAC,OAAO,CAAC,UAAU,CAAC;IACzC,iBAAiB,CAAC,OAAO,CAAC,UAAU,CAAC;IACrC,UAAU,CAAC,OAAO,CAAC;CACtB,CAAC;AAEF,eAAe,IAAI,CAAC","sourcesContent":["import type { Plugin } from \"vite\";\nimport gtkxVitest, { type PluginOptions } from \"@gtkx/vitest\";\nimport { gtkxEnsureStore } from \"./vite-plugins/ensure-store.js\";\nimport { gtkxFontWorkerEnv } from \"./vite-plugins/font-worker-env.js\";\nimport { gtkxVitePlugins } from \"./vite-plugins/index.js\";\nimport { gtkxSettingsWorkerEnv } from \"./vite-plugins/settings-worker-env.js\";\n\n/**\n * Vite plugin stack for testing a GTKX application: the generated bindings the tests import,\n * the plugins the app is built with, the project's compiled GSettings schemas exposed to every\n * test worker, and a headless display per worker.\n *\n * @returns The plugins to spread into a Vitest config.\n */\nconst gtkx = (options: PluginOptions = {}): Plugin[] => [\n gtkxEnsureStore(options.configFile),\n ...gtkxVitePlugins({ configFile: options.configFile }),\n gtkxSettingsWorkerEnv(options.configFile),\n gtkxFontWorkerEnv(options.configFile),\n gtkxVitest(options),\n];\n\nexport default gtkx;\n"]}
package/env.d.ts CHANGED
@@ -14,6 +14,15 @@ declare module "*?icon" {
14
14
  export default iconName;
15
15
  }
16
16
 
17
+ declare module "*?font" {
18
+ /**
19
+ * Font family name of the bundled font. Importing the module bundles the font file with the application and
20
+ * puts it where fontconfig finds it, so the family is available to Pango without any runtime registration.
21
+ */
22
+ const fontFamily: string;
23
+ export default fontFamily;
24
+ }
25
+
17
26
  declare module "*.css?url" {
18
27
  /**
19
28
  * Absolute filesystem path of the source stylesheet under `gtkx dev` or the emitted stylesheet once built,
package/package.json CHANGED
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "name": "@gtkx/cli",
3
- "version": "2.0.0-beta.6",
4
- "description": "GTK app toolchain for React: scaffold, dev server, build, docs.",
3
+ "version": "2.0.0-beta.8",
4
+ "description": "Adwaita-first GNOME app toolchain for React and TypeScript.",
5
5
  "keywords": [
6
6
  "gtkx",
7
7
  "gtk",
8
8
  "gtk4",
9
+ "adwaita",
10
+ "gnome",
9
11
  "react",
10
12
  "typescript",
11
13
  "native",
@@ -70,33 +72,33 @@
70
72
  "dependencies": {
71
73
  "@babel/core": "^8.0.1",
72
74
  "@babel/preset-typescript": "^8.0.1",
73
- "@gtkx/codegen": "2.0.0-beta.6",
74
- "@gtkx/config": "2.0.0-beta.6",
75
- "@gtkx/mcp": "2.0.0-beta.6",
76
- "@gtkx/native": "2.0.0-beta.6",
77
- "@gtkx/runtime": "2.0.0-beta.6",
78
- "@gtkx/utils": "2.0.0-beta.6",
79
- "@gtkx/vitest": "2.0.0-beta.6",
80
- "@swc/core": "^1.16.1",
75
+ "@gtkx/codegen": "2.0.0-beta.8",
76
+ "@gtkx/config": "2.0.0-beta.8",
77
+ "@gtkx/mcp": "2.0.0-beta.8",
78
+ "@gtkx/native": "2.0.0-beta.8",
79
+ "@gtkx/runtime": "2.0.0-beta.8",
80
+ "@gtkx/utils": "2.0.0-beta.8",
81
+ "@gtkx/vitest": "2.0.0-beta.8",
82
+ "@swc/core": "^1.16.2",
81
83
  "babel-plugin-react-compiler": "^1.0.0",
82
84
  "citty": "^0.2.2",
83
- "confbox": "^0.2.4",
84
- "create-gtkx": "2.0.0-beta.6",
85
+ "confbox": "^0.3.1",
86
+ "create-gtkx": "2.0.0-beta.8",
85
87
  "fast-xml-builder": "^1.3.1",
86
88
  "fast-xml-parser": "^5.11.1",
87
89
  "hosted-git-info": "^10.1.1",
88
- "i18next-cli": "1.72.1",
90
+ "i18next-cli": "1.73.1",
89
91
  "jiti": "^2.7.0",
90
92
  "react-refresh": "^0.18.0",
91
93
  "vite": "^8.2.2",
92
94
  "yaml": "^2.9.0"
93
95
  },
94
96
  "devDependencies": {
95
- "@gtkx/testing": "2.0.0-beta.6",
97
+ "@gtkx/testing": "2.0.0-beta.8",
96
98
  "@types/babel__core": "^7.20.5",
97
99
  "@types/hosted-git-info": "^3.0.5",
98
100
  "@types/react-refresh": "^0.14.7",
99
- "vitest": "^4.1.11"
101
+ "vitest": "^5.0.0"
100
102
  },
101
103
  "peerDependencies": {
102
104
  "react": "^19"
package/src/cli.ts CHANGED
@@ -18,7 +18,7 @@ const main = defineCommand({
18
18
  meta: {
19
19
  name: "gtkx",
20
20
  version,
21
- description: "CLI for GTKX: create and develop GTK4 React applications",
21
+ description: "CLI for GTKX: create and develop Adwaita-first GNOME applications",
22
22
  },
23
23
  subCommands: {
24
24
  dev: async () => {
@@ -1,5 +1,6 @@
1
1
  import type { Config } from "@gtkx/config";
2
2
  import { resolveGirPath, resolveLibraries } from "@gtkx/codegen";
3
+ import { resolveBoundLibraries } from "@gtkx/codegen/internal";
3
4
  import { existsSync, readFileSync } from "node:fs";
4
5
  import { basename, dirname, join } from "node:path";
5
6
  import { type CodegenStore, resolveCodegenStore } from "./store-resolver.js";
@@ -86,7 +87,10 @@ const hasStoreExports = (storeDir: string, required: string[], forbidden: string
86
87
  };
87
88
 
88
89
  const isGiStoreStale = (store: CodegenStore, libraries: string[]): boolean => {
89
- const required = [PACKAGE_EXPORT, ...libraries.map((library) => namespaceExport(library))];
90
+ const required = [
91
+ PACKAGE_EXPORT,
92
+ ...resolveBoundLibraries(libraries).map((library) => namespaceExport(library)),
93
+ ];
90
94
 
91
95
  return !hasStoreExports(store.giStoreDir, required);
92
96
  };
@@ -1,6 +1,7 @@
1
1
  import { defineCommand } from "citty";
2
2
  import { KNOWN_NAMES } from "../deploy/registry.js";
3
3
  import { runDeploy } from "../deploy/run-deploy.js";
4
+ import { KNOWN_ARCH_NAMES } from "../deploy/settings/arch.js";
4
5
  import { configArg, entryArg } from "../internal/entry-arg.js";
5
6
  import { resolveProject } from "../internal/prepare-project.js";
6
7
 
@@ -18,6 +19,10 @@ const deploy = defineCommand({
18
19
  type: "string",
19
20
  description: `Comma-separated package formats to build (${KNOWN_NAMES})`,
20
21
  },
22
+ arch: {
23
+ type: "string",
24
+ description: `Comma-separated architectures to build (${KNOWN_ARCH_NAMES}; default: the host)`,
25
+ },
21
26
  out: {
22
27
  type: "string",
23
28
  description: "Output directory relative to the project root (default: build)",
@@ -39,6 +44,7 @@ const deploy = defineCommand({
39
44
  cwd,
40
45
  configFile,
41
46
  targets: args.target,
47
+ arches: args.arch,
42
48
  outDir: args.out,
43
49
  shouldPrintManifests: args["print-manifests"] === true,
44
50
  shouldSkipBuild: args["skip-build"] === true,
@@ -11,17 +11,23 @@ type DigestRequest = {
11
11
  subject: string;
12
12
  };
13
13
 
14
+ type DigestAlgorithm = "sha256" | "sha512";
15
+
14
16
  type DownloadRequest = {
15
17
  url: string;
16
18
  dest: string;
17
19
  label: string;
18
- sha256: string;
19
- freshSha256?: (() => Promise<string>) | undefined;
20
+ digest: string;
21
+ algorithm?: DigestAlgorithm | undefined;
22
+ freshDigest?: (() => Promise<string>) | undefined;
20
23
  mode?: number | undefined;
21
24
  };
22
25
 
23
26
  const CACHE_NAMESPACE = "gtkx";
24
- const DIGEST_PATTERN = /^[\da-f]{64}$/;
27
+ const DIGEST_PATTERN: Record<DigestAlgorithm, RegExp> = {
28
+ sha256: /^[\da-f]{64}$/,
29
+ sha512: /^[\da-f]{128}$/,
30
+ };
25
31
 
26
32
  const cacheRoot = (): string => {
27
33
  const base = process.env.XDG_CACHE_HOME;
@@ -36,10 +42,11 @@ const cacheDir = (segments: string[]): string => {
36
42
  return dir;
37
43
  };
38
44
 
39
- const getDigest = (contents: Buffer): string => createHash("sha256").update(contents).digest("hex");
45
+ const getDigest = (contents: Buffer, algorithm: DigestAlgorithm): string =>
46
+ createHash(algorithm).update(contents).digest("hex");
40
47
 
41
- const assertDigest = (url: string, contents: Buffer, expected: string): void => {
42
- const actual = getDigest(contents);
48
+ const assertDigest = (url: string, contents: Buffer, expected: string, algorithm: DigestAlgorithm): void => {
49
+ const actual = getDigest(contents, algorithm);
43
50
 
44
51
  if (actual !== expected) {
45
52
  throw new Error(`Checksum mismatch for ${url}\n expected ${expected}\n received ${actual}`);
@@ -62,11 +69,11 @@ const fetchText = async (url: string): Promise<string> => {
62
69
  return bytes.toString("utf8");
63
70
  };
64
71
 
65
- const readCachedDigest = (dest: string): string | undefined => {
72
+ const readCachedDigest = (dest: string, algorithm: DigestAlgorithm): string | undefined => {
66
73
  try {
67
74
  const cached = readFileSync(dest, "utf8").trim();
68
75
 
69
- return DIGEST_PATTERN.test(cached) ? cached : undefined;
76
+ return DIGEST_PATTERN[algorithm].test(cached) ? cached : undefined;
70
77
  } catch {
71
78
  return undefined;
72
79
  }
@@ -82,7 +89,7 @@ const publishedDigest = async (request: DigestRequest): Promise<string> => {
82
89
  const checksums = await fetchText(request.url);
83
90
  const digest = digestFromChecksums(checksums, request.assetName, request.subject);
84
91
 
85
- if (!DIGEST_PATTERN.test(digest)) {
92
+ if (!DIGEST_PATTERN.sha256.test(digest)) {
86
93
  throw new Error(`${request.subject} published a malformed checksum for ${request.assetName}: "${digest}"`);
87
94
  }
88
95
 
@@ -92,17 +99,17 @@ const publishedDigest = async (request: DigestRequest): Promise<string> => {
92
99
  };
93
100
 
94
101
  const cachedDigest = async (request: DigestRequest): Promise<string> => {
95
- const cached = readCachedDigest(request.dest);
102
+ const cached = readCachedDigest(request.dest, "sha256");
96
103
 
97
104
  return cached ?? publishedDigest(request);
98
105
  };
99
106
 
100
- const isCacheUsable = (dest: string, sha256: string): boolean => {
107
+ const isCacheUsable = (dest: string, digest: string, algorithm: DigestAlgorithm): boolean => {
101
108
  if (!existsSync(dest)) {
102
109
  return false;
103
110
  }
104
111
 
105
- if (getDigest(readFileSync(dest)) === sha256) {
112
+ if (getDigest(readFileSync(dest), algorithm) === digest) {
106
113
  return true;
107
114
  }
108
115
 
@@ -112,16 +119,17 @@ const isCacheUsable = (dest: string, sha256: string): boolean => {
112
119
  };
113
120
 
114
121
  const downloadFile = async (request: DownloadRequest): Promise<string> => {
115
- const { url, dest, label, sha256, mode } = request;
122
+ const { url, dest, label, digest, mode } = request;
123
+ const algorithm = request.algorithm ?? "sha256";
116
124
 
117
- if (isCacheUsable(dest, sha256)) {
125
+ if (isCacheUsable(dest, digest, algorithm)) {
118
126
  return dest;
119
127
  }
120
128
 
121
129
  info(`Downloading ${label}`);
122
- const expected = request.freshSha256 === undefined ? sha256 : await request.freshSha256();
130
+ const expected = request.freshDigest === undefined ? digest : await request.freshDigest();
123
131
  const contents = await fetchBytes(url);
124
- assertDigest(url, contents, expected);
132
+ assertDigest(url, contents, expected, algorithm);
125
133
  const staging = `${dest}.partial`;
126
134
  writeFileSync(staging, contents);
127
135
 
@@ -145,4 +153,12 @@ const digestFromChecksums = (checksums: string, assetName: string, subject: stri
145
153
  return digest;
146
154
  };
147
155
 
148
- export { cacheDir, cachedDigest, type DigestRequest, downloadFile, publishedDigest };
156
+ export {
157
+ cacheDir,
158
+ cachedDigest,
159
+ type DigestRequest,
160
+ downloadFile,
161
+ publishedDigest,
162
+ readCachedDigest,
163
+ writeAtomically,
164
+ };
@@ -0,0 +1,183 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import { dirname, join } from "node:path";
4
+ import type { DeployArchName, DeploySettings } from "./types.js";
5
+ import { runCliTool } from "../internal/run-cli-tool.js";
6
+ import { cacheDir, downloadFile, readCachedDigest, writeAtomically } from "./download.js";
7
+ import { isHostArch } from "./settings/arch.js";
8
+
9
+ type ProjectRequire = ReturnType<typeof createRequire>;
10
+ type Tarball = { url: string; digest: string };
11
+
12
+ const BINDING_FILENAME = "gtkx.node";
13
+ const DEFAULT_REGISTRY = "https://registry.npmjs.org";
14
+ const NATIVE_MANIFEST = "@gtkx/native/package.json";
15
+ const PACKAGE_ROOT = "package";
16
+ const REQUEST_TIMEOUT = 30_000;
17
+ const SRI_PREFIX = "sha512-";
18
+ const STRIP_COMPONENTS = "1";
19
+ const VERSION_PATTERN = /^[\w.+-]+$/;
20
+
21
+ const binaryFilename = (arch: DeployArchName): string => `native.linux-${arch}-gnu.node`;
22
+ const platformStem = (arch: DeployArchName): string => `native-linux-${arch}-gnu`;
23
+ const platformPackage = (arch: DeployArchName): string => `@gtkx/${platformStem(arch)}`;
24
+
25
+ const withoutTrailingSlash = (value: string): string => {
26
+ let end = value.length;
27
+
28
+ while (end > 0 && value[end - 1] === "/") {
29
+ end -= 1;
30
+ }
31
+
32
+ return value.slice(0, end);
33
+ };
34
+
35
+ const registryUrl = (): string => {
36
+ const configured = process.env.npm_config_registry;
37
+
38
+ return withoutTrailingSlash(configured !== undefined && configured.length > 0 ? configured : DEFAULT_REGISTRY);
39
+ };
40
+
41
+ const projectRequireFor = (root: string): ProjectRequire => createRequire(join(root, "package.json"));
42
+
43
+ const localBinary = (projectRequire: ProjectRequire, arch: DeployArchName): string | null => {
44
+ const nativeRoot = dirname(projectRequire.resolve(NATIVE_MANIFEST));
45
+ const candidate = join(nativeRoot, binaryFilename(arch));
46
+
47
+ return existsSync(candidate) ? candidate : null;
48
+ };
49
+
50
+ const installedBinary = (projectRequire: ProjectRequire, arch: DeployArchName): string | null => {
51
+ try {
52
+ return projectRequire.resolve(platformPackage(arch));
53
+ } catch {
54
+ return null;
55
+ }
56
+ };
57
+
58
+ const manifestVersion = (manifest: unknown): string | null => {
59
+ if (typeof manifest !== "object" || manifest === null || !("version" in manifest)) {
60
+ return null;
61
+ }
62
+
63
+ const version = manifest.version;
64
+
65
+ return typeof version === "string" && VERSION_PATTERN.test(version) ? version : null;
66
+ };
67
+
68
+ const nativeVersion = (projectRequire: ProjectRequire): string => {
69
+ const parsed: unknown = JSON.parse(readFileSync(projectRequire.resolve(NATIVE_MANIFEST), "utf8"));
70
+ const version = manifestVersion(parsed);
71
+
72
+ if (version === null) {
73
+ throw new Error("Cannot read a usable version from @gtkx/native; is it installed?");
74
+ }
75
+
76
+ return version;
77
+ };
78
+
79
+ const hexFromIntegrity = (integrity: unknown, subject: string): string => {
80
+ if (typeof integrity !== "string" || !integrity.startsWith(SRI_PREFIX)) {
81
+ throw new Error(`The npm registry publishes no sha512 integrity for ${subject}`);
82
+ }
83
+
84
+ return Buffer.from(integrity.slice(SRI_PREFIX.length), "base64").toString("hex");
85
+ };
86
+
87
+ const fetchVersionDocument = async (url: string, subject: string): Promise<Record<string, unknown>> => {
88
+ const response = await fetch(url, { signal: AbortSignal.timeout(REQUEST_TIMEOUT) });
89
+
90
+ if (!response.ok) {
91
+ throw new Error(
92
+ `Cannot fetch ${subject}: HTTP ${String(response.status)} ${response.statusText}. ` +
93
+ `Install ${subject} directly, or drop that architecture from --arch.`,
94
+ );
95
+ }
96
+
97
+ const document: unknown = await response.json();
98
+
99
+ if (typeof document !== "object" || document === null) {
100
+ throw new Error(`The npm registry returned no metadata for ${subject}`);
101
+ }
102
+
103
+ return document as Record<string, unknown>;
104
+ };
105
+
106
+ const publishedTarball = async (arch: DeployArchName, version: string): Promise<Tarball> => {
107
+ const registry = registryUrl();
108
+ const subject = `${platformPackage(arch)}@${version}`;
109
+ const url = `${registry}/${encodeURIComponent(platformPackage(arch))}/${version}`;
110
+ const document = await fetchVersionDocument(url, subject);
111
+ const dist: unknown = document.dist;
112
+
113
+ if (typeof dist !== "object" || dist === null) {
114
+ throw new Error(`The npm registry returned no tarball for ${subject}`);
115
+ }
116
+
117
+ const entry = dist as Record<string, unknown>;
118
+ const tarball = entry.tarball;
119
+
120
+ if (typeof tarball !== "string" || new URL(tarball).origin !== new URL(registry).origin) {
121
+ throw new Error(`The npm registry returned an unusable tarball URL for ${subject}`);
122
+ }
123
+
124
+ return { url: tarball, digest: hexFromIntegrity(entry.integrity, subject) };
125
+ };
126
+
127
+ const extractAddon = (archive: string, dir: string, arch: DeployArchName): void => {
128
+ runCliTool({
129
+ tool: "tar",
130
+ args: ["-xzf", archive, "-C", dir, "--strip-components", STRIP_COMPONENTS,
131
+ `${PACKAGE_ROOT}/${binaryFilename(arch)}`],
132
+ target: platformPackage(arch),
133
+ });
134
+ };
135
+
136
+ const cachedTarballUrl = (arch: DeployArchName, version: string): string =>
137
+ `${registryUrl()}/${platformPackage(arch)}/-/${platformStem(arch)}-${version}.tgz`;
138
+
139
+ const tarballFor = async (arch: DeployArchName, version: string, sidecar: string): Promise<Tarball> => {
140
+ const cached = readCachedDigest(sidecar, "sha512");
141
+
142
+ if (cached !== undefined) {
143
+ return { url: cachedTarballUrl(arch, version), digest: cached };
144
+ }
145
+
146
+ const published = await publishedTarball(arch, version);
147
+ writeAtomically(sidecar, published.digest);
148
+
149
+ return published;
150
+ };
151
+
152
+ const downloadedBinary = async (arch: DeployArchName, version: string): Promise<string> => {
153
+ const dir = cacheDir(["native", `${version}-linux-${arch}`]);
154
+ const archive = join(dir, `${platformStem(arch)}-${version}.tgz`);
155
+ const tarball = await tarballFor(arch, version, `${archive}.sha512`);
156
+
157
+ await downloadFile({
158
+ url: tarball.url,
159
+ dest: archive,
160
+ label: `${platformPackage(arch)} ${version}`,
161
+ digest: tarball.digest,
162
+ algorithm: "sha512",
163
+ });
164
+
165
+ extractAddon(archive, dir, arch);
166
+
167
+ return join(dir, binaryFilename(arch));
168
+ };
169
+
170
+ const resolveStagedAddon = async (settings: DeploySettings): Promise<string | null> => {
171
+ const arch = settings.arch.node;
172
+
173
+ if (isHostArch(arch)) {
174
+ return null;
175
+ }
176
+
177
+ const projectRequire = projectRequireFor(settings.paths.root);
178
+ const resolved = localBinary(projectRequire, arch) ?? installedBinary(projectRequire, arch);
179
+
180
+ return resolved ?? await downloadedBinary(arch, nativeVersion(projectRequire));
181
+ };
182
+
183
+ export { BINDING_FILENAME, resolveStagedAddon };
@@ -44,8 +44,8 @@ const downloadNode = async (version: string, arch: string, destDir: string): Pro
44
44
  url: `${DIST_BASE_URL}/v${version}/${assetName}`,
45
45
  dest: join(dir, assetName),
46
46
  label: `Node.js ${version} for linux-${arch}`,
47
- sha256: await cachedDigest(request),
48
- freshSha256: () => publishedDigest(request),
47
+ digest: await cachedDigest(request),
48
+ freshDigest: () => publishedDigest(request),
49
49
  });
50
50
 
51
51
  mkdirSync(destDir, { recursive: true });
@@ -7,6 +7,7 @@ type ElfSection = {
7
7
  };
8
8
 
9
9
  type ElfInfo = {
10
+ machine: number;
10
11
  needed: string[];
11
12
  glibcMinimum: string | null;
12
13
  };
@@ -22,6 +23,7 @@ const CLASS_64 = 2;
22
23
  const LITTLE_ENDIAN = 1;
23
24
  const OFFSET_CLASS = 4;
24
25
  const OFFSET_DATA = 5;
26
+ const OFFSET_MACHINE = 0x12;
25
27
  const OFFSET_SECTION_HEADERS = 0x28;
26
28
  const OFFSET_SECTION_ENTRY_SIZE = 0x3A;
27
29
  const OFFSET_SECTION_COUNT = 0x3C;
@@ -140,6 +142,7 @@ const readElfBuffer = (buffer: Buffer, path: string): ElfInfo => {
140
142
  const strings = stringTable(buffer, sectionFor(sections, ".dynstr"));
141
143
 
142
144
  return {
145
+ machine: buffer.readUInt16LE(OFFSET_MACHINE),
143
146
  needed: readNeeded(buffer, sectionFor(sections, ".dynamic"), strings),
144
147
  glibcMinimum: readGlibcMinimum(strings),
145
148
  };
@@ -180,4 +183,4 @@ const glibcMinimumForFiles = (paths: Iterable<string>): string | null => {
180
183
  return formatVersion(highestVersion(versions));
181
184
  };
182
185
 
183
- export { type ElfInfo, glibcMinimumForFiles, readElfInfo };
186
+ export { type ElfInfo, glibcMinimumForFiles, readElfInfo, readOptionalElfInfo };
@@ -1,12 +1,13 @@
1
1
  import { MINIMUM_NODE_VERSION } from "@gtkx/config/internal";
2
- import { info, tryResolveExecutable } from "@gtkx/utils";
2
+ import { info, tryResolveExecutable, warn } from "@gtkx/utils";
3
3
  import { execFileSync } from "node:child_process";
4
4
  import { chmodSync, copyFileSync, mkdirSync, statSync } from "node:fs";
5
5
  import { join, resolve } from "node:path";
6
- import type { DeploySettings, NodeRuntime } from "../types.js";
6
+ import type { DeployConfig, DeploySettings, NodeRuntime } from "../types.js";
7
7
  import { runCliTool } from "../../internal/run-cli-tool.js";
8
+ import { elfMachineFor, hostArchName } from "../settings/arch.js";
8
9
  import { downloadNode } from "./download.js";
9
- import { readElfInfo } from "./elf.js";
10
+ import { type ElfInfo, readElfInfo } from "./elf.js";
10
11
  import { assertPortableNode } from "./guard.js";
11
12
  import { licenseBesideNode } from "./license.js";
12
13
 
@@ -171,13 +172,32 @@ const stageFromSource = async (
171
172
  return { path: stageNode(settings, sourcePath), licenseFile: licenseBesideNode(sourcePath) };
172
173
  };
173
174
 
175
+ type DeployNodeConfig = NonNullable<DeployConfig["node"]>;
176
+
177
+ const shouldStripRuntime = (settings: DeploySettings, node: DeployNodeConfig, elf: ElfInfo): boolean => {
178
+ if (node.shouldStrip === false) {
179
+ return false;
180
+ }
181
+
182
+ if (elf.machine === elfMachineFor(hostArchName())) {
183
+ return true;
184
+ }
185
+
186
+ warn(
187
+ `Skipping \`strip\` on the bundled Node.js: it was built for ${settings.arch.node} and \`strip\` reads ` +
188
+ `only ${hostArchName()}. The ${settings.arch.node} packages carry an unstripped runtime.`,
189
+ );
190
+
191
+ return false;
192
+ };
193
+
174
194
  const resolveNodeRuntime = async (settings: DeploySettings): Promise<NodeRuntime> => {
175
195
  const node = settings.deploy.node ?? {};
176
196
  const source = nodeSourceFor(settings);
177
197
  const version = resolveNodeVersion(settings);
178
198
  const staged = await stageFromSource(settings, version, source);
179
- const isStripped = node.shouldStrip === false ? false : didStripBinary(staged.path);
180
199
  const elf = readElfInfo(staged.path);
200
+ const isStripped = shouldStripRuntime(settings, node, elf) && didStripBinary(staged.path);
181
201
  const glibcMinimum = elf.glibcMinimum ?? "unknown";
182
202
  info(`Bundled Node.js v${version} (${megabytes(staged.path)} MiB, runtime glibc >= ${glibcMinimum})`);
183
203
 
@@ -8,7 +8,10 @@ import { listFilesRecursive } from "../../internal/list-files.js";
8
8
  import { BUNDLE_FILENAME } from "../../vite-plugins/esm-extension.js";
9
9
  import { renderCopyright } from "../freedesktop/copyright.js";
10
10
  import { renderDbusService } from "../freedesktop/dbus-service.js";
11
+ import { BINDING_FILENAME } from "../native-addon.js";
12
+ import { readOptionalElfInfo } from "../node-runtime/elf.js";
11
13
  import { renderNotices } from "../notices/render.js";
14
+ import { elfMachineFor } from "../settings/arch.js";
12
15
  import { copyInto, EXECUTABLE_MODE, executableModeFor, writeInto } from "./copy-tree.js";
13
16
  import { stageIcons } from "./icons.js";
14
17
  import { NODE_FILENAME, renderLauncher } from "./launcher.js";
@@ -17,6 +20,7 @@ import { stageSchemas } from "./schemas.js";
17
20
  type StageRequest = {
18
21
  settings: DeploySettings;
19
22
  node: NodeRuntime | null;
23
+ addon: string | null;
20
24
  metadata: StagedMetadata;
21
25
  };
22
26
 
@@ -67,6 +71,29 @@ const stageRuntimeFiles = (settings: DeploySettings, root: string): StagedFile[]
67
71
  .map((file) => copyInto(root, join(libDirFor(settings), file.rel), file.absPath));
68
72
  };
69
73
 
74
+ const stageAddon = (settings: DeploySettings, root: string, addon: string | null): StagedFile[] =>
75
+ addon === null ? [] : [copyInto(root, join(libDirFor(settings), BINDING_FILENAME), addon)];
76
+
77
+ const architectureMismatch = (settings: DeploySettings, file: StagedFile): Error =>
78
+ new Error(
79
+ `Cannot deploy ${file.rel} for ${settings.arch.node}: it was built for another architecture. ` +
80
+ "Run `gtkx build` again, or drop --skip-build.",
81
+ );
82
+
83
+ const assertStagedArchitecture = (settings: DeploySettings, staged: StagedFile[]): void => {
84
+ const expected = elfMachineFor(settings.arch.node);
85
+ const lib = libDirFor(settings);
86
+ const checked = new Set([join(lib, BINDING_FILENAME), join(lib, NODE_FILENAME)]);
87
+
88
+ const binaries = staged.filter((entry) => checked.has(entry.rel));
89
+
90
+ for (const file of binaries) {
91
+ if (readOptionalElfInfo(file.abs)?.machine !== expected) {
92
+ throw architectureMismatch(settings, file);
93
+ }
94
+ }
95
+ };
96
+
70
97
  const stageCatalogs = (settings: DeploySettings, root: string): StagedFile[] =>
71
98
  listFilesRecursive(join(settings.paths.dist, LOCALE_DIRNAME))
72
99
  .map((file) => copyInto(root, join(SHARE_LOCALE, file.rel), file.absPath));
@@ -138,20 +165,25 @@ const byRelativePath = (files: StagedFile[]): StagedFile[] => {
138
165
  return sortStringsBy(latest.values(), (file) => file.rel);
139
166
  };
140
167
 
141
- const stagePayload = ({ settings, node, metadata }: StageRequest): StagedFile[] => {
168
+ const stagePayload = ({ settings, node, addon, metadata }: StageRequest): StagedFile[] => {
142
169
  const root = settings.paths.stage;
143
170
  rmSync(root, { recursive: true, force: true, maxRetries: 5 });
144
171
 
145
- return byRelativePath([
172
+ const staged = byRelativePath([
146
173
  writeInto(root, join("bin", settings.binaryName), renderLauncher(settings), EXECUTABLE_MODE),
147
174
  ...stageNodeBinary(settings, root, node),
148
175
  ...stageRuntimeFiles(settings, root),
176
+ ...stageAddon(settings, root, addon),
149
177
  ...stageCatalogs(settings, root),
150
178
  ...stageExtraFiles(settings, root, metadata),
151
179
  ...stageMetadata(settings, root, metadata),
152
180
  ...stageIcons(settings, root),
153
181
  ...stageSchemas(settings, root),
154
182
  ]);
183
+
184
+ assertStagedArchitecture(settings, staged);
185
+
186
+ return staged;
155
187
  };
156
188
 
157
189
  const withoutDbusActivation = (desktopEntry: string): string =>