@gtkx/cli 1.3.0 → 1.4.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.
Files changed (217) hide show
  1. package/README.md +5 -4
  2. package/dist/builder.d.ts +0 -1
  3. package/dist/builder.d.ts.map +1 -1
  4. package/dist/builder.js +6 -6
  5. package/dist/builder.js.map +1 -1
  6. package/dist/codegen/run-codegen.d.ts +1 -2
  7. package/dist/codegen/run-codegen.d.ts.map +1 -1
  8. package/dist/codegen/run-codegen.js +11 -6
  9. package/dist/codegen/run-codegen.js.map +1 -1
  10. package/dist/commands/build.d.ts +0 -4
  11. package/dist/commands/build.d.ts.map +1 -1
  12. package/dist/commands/build.js +0 -5
  13. package/dist/commands/build.js.map +1 -1
  14. package/dist/commands/codegen.d.ts.map +1 -1
  15. package/dist/commands/codegen.js +1 -3
  16. package/dist/commands/codegen.js.map +1 -1
  17. package/dist/deploy/depends.d.ts +2 -2
  18. package/dist/deploy/depends.d.ts.map +1 -1
  19. package/dist/deploy/depends.js +30 -15
  20. package/dist/deploy/depends.js.map +1 -1
  21. package/dist/deploy/nfpm/config.js +1 -1
  22. package/dist/deploy/nfpm/config.js.map +1 -1
  23. package/dist/deploy/node-runtime/elf.d.ts +1 -1
  24. package/dist/deploy/node-runtime/elf.d.ts.map +1 -1
  25. package/dist/deploy/node-runtime/elf.js +2 -2
  26. package/dist/deploy/node-runtime/elf.js.map +1 -1
  27. package/dist/deploy/node-runtime/index.js +2 -2
  28. package/dist/deploy/node-runtime/index.js.map +1 -1
  29. package/dist/deploy/notices/collect.d.ts +3 -1
  30. package/dist/deploy/notices/collect.d.ts.map +1 -1
  31. package/dist/deploy/notices/collect.js +2 -2
  32. package/dist/deploy/notices/collect.js.map +1 -1
  33. package/dist/deploy/notices/packages.d.ts +2 -1
  34. package/dist/deploy/notices/packages.d.ts.map +1 -1
  35. package/dist/deploy/notices/packages.js +4 -27
  36. package/dist/deploy/notices/packages.js.map +1 -1
  37. package/dist/deploy/payload/icons.d.ts +7 -3
  38. package/dist/deploy/payload/icons.d.ts.map +1 -1
  39. package/dist/deploy/payload/icons.js +46 -28
  40. package/dist/deploy/payload/icons.js.map +1 -1
  41. package/dist/deploy/payload/stage.d.ts.map +1 -1
  42. package/dist/deploy/payload/stage.js +3 -2
  43. package/dist/deploy/payload/stage.js.map +1 -1
  44. package/dist/deploy/run-deploy.d.ts.map +1 -1
  45. package/dist/deploy/run-deploy.js +63 -11
  46. package/dist/deploy/run-deploy.js.map +1 -1
  47. package/dist/deploy/settings/build-manifest.d.ts +9 -0
  48. package/dist/deploy/settings/build-manifest.d.ts.map +1 -0
  49. package/dist/deploy/settings/build-manifest.js +80 -0
  50. package/dist/deploy/settings/build-manifest.js.map +1 -0
  51. package/dist/deploy/settings/index.d.ts.map +1 -1
  52. package/dist/deploy/settings/index.js +9 -3
  53. package/dist/deploy/settings/index.js.map +1 -1
  54. package/dist/deploy/settings/libraries.d.ts +8 -0
  55. package/dist/deploy/settings/libraries.d.ts.map +1 -0
  56. package/dist/deploy/settings/libraries.js +39 -0
  57. package/dist/deploy/settings/libraries.js.map +1 -0
  58. package/dist/deploy/settings/paths.d.ts +3 -0
  59. package/dist/deploy/settings/paths.d.ts.map +1 -1
  60. package/dist/deploy/settings/paths.js +6 -32
  61. package/dist/deploy/settings/paths.js.map +1 -1
  62. package/dist/deploy/targets/appimage.d.ts.map +1 -1
  63. package/dist/deploy/targets/appimage.js +19 -13
  64. package/dist/deploy/targets/appimage.js.map +1 -1
  65. package/dist/deploy/targets/flatpak-manifest.d.ts +2 -1
  66. package/dist/deploy/targets/flatpak-manifest.d.ts.map +1 -1
  67. package/dist/deploy/targets/flatpak-manifest.js +5 -1
  68. package/dist/deploy/targets/flatpak-manifest.js.map +1 -1
  69. package/dist/deploy/targets/flatpak-source.d.ts.map +1 -1
  70. package/dist/deploy/targets/flatpak-source.js +9 -6
  71. package/dist/deploy/targets/flatpak-source.js.map +1 -1
  72. package/dist/deploy/types.d.ts +4 -4
  73. package/dist/deploy/types.d.ts.map +1 -1
  74. package/dist/deploy/types.js.map +1 -1
  75. package/dist/dev/icon-dir.d.ts +1 -1
  76. package/dist/dev/icon-dir.d.ts.map +1 -1
  77. package/dist/dev/icon-dir.js +20 -7
  78. package/dist/dev/icon-dir.js.map +1 -1
  79. package/dist/dev/runner-deps.d.ts.map +1 -1
  80. package/dist/dev/runner-deps.js +5 -1
  81. package/dist/dev/runner-deps.js.map +1 -1
  82. package/dist/dev/runner-main.js +4 -4
  83. package/dist/dev/runner-main.js.map +1 -1
  84. package/dist/dev/runner.d.ts +1 -1
  85. package/dist/dev/runner.d.ts.map +1 -1
  86. package/dist/dev/runner.js +1 -1
  87. package/dist/dev/runner.js.map +1 -1
  88. package/dist/dev/schema-dir.d.ts +1 -1
  89. package/dist/dev/schema-dir.d.ts.map +1 -1
  90. package/dist/dev/schema-dir.js +3 -1
  91. package/dist/dev/schema-dir.js.map +1 -1
  92. package/dist/internal/build-manifest.d.ts +20 -0
  93. package/dist/internal/build-manifest.d.ts.map +1 -0
  94. package/dist/internal/build-manifest.js +6 -0
  95. package/dist/internal/build-manifest.js.map +1 -0
  96. package/dist/internal/icon-path.d.ts +13 -0
  97. package/dist/internal/icon-path.d.ts.map +1 -0
  98. package/dist/internal/icon-path.js +86 -0
  99. package/dist/internal/icon-path.js.map +1 -0
  100. package/dist/internal/source-imports.d.ts +8 -0
  101. package/dist/internal/source-imports.d.ts.map +1 -0
  102. package/dist/internal/source-imports.js +144 -0
  103. package/dist/internal/source-imports.js.map +1 -0
  104. package/dist/mcp/widget-properties.d.ts.map +1 -1
  105. package/dist/mcp/widget-properties.js +3 -12
  106. package/dist/mcp/widget-properties.js.map +1 -1
  107. package/dist/settings/render.d.ts +7 -1
  108. package/dist/settings/render.d.ts.map +1 -1
  109. package/dist/settings/render.js +69 -20
  110. package/dist/settings/render.js.map +1 -1
  111. package/dist/settings/schema.d.ts +4 -3
  112. package/dist/settings/schema.d.ts.map +1 -1
  113. package/dist/settings/schema.js +122 -15
  114. package/dist/settings/schema.js.map +1 -1
  115. package/dist/vite-plugins/asset-emitter.d.ts +1 -1
  116. package/dist/vite-plugins/asset-emitter.d.ts.map +1 -1
  117. package/dist/vite-plugins/asset-emitter.js.map +1 -1
  118. package/dist/vite-plugins/asset-extensions.d.ts.map +1 -1
  119. package/dist/vite-plugins/asset-extensions.js +1 -0
  120. package/dist/vite-plugins/asset-extensions.js.map +1 -1
  121. package/dist/vite-plugins/asset-imports.d.ts +2 -1
  122. package/dist/vite-plugins/asset-imports.d.ts.map +1 -1
  123. package/dist/vite-plugins/asset-imports.js +37 -16
  124. package/dist/vite-plugins/asset-imports.js.map +1 -1
  125. package/dist/vite-plugins/asset-specifier.d.ts +13 -1
  126. package/dist/vite-plugins/asset-specifier.d.ts.map +1 -1
  127. package/dist/vite-plugins/asset-specifier.js +39 -1
  128. package/dist/vite-plugins/asset-specifier.js.map +1 -1
  129. package/dist/vite-plugins/build-manifest.d.ts +5 -0
  130. package/dist/vite-plugins/build-manifest.d.ts.map +1 -0
  131. package/dist/vite-plugins/{bundled-packages.js → build-manifest.js} +20 -14
  132. package/dist/vite-plugins/build-manifest.js.map +1 -0
  133. package/dist/vite-plugins/built-url.d.ts +1 -1
  134. package/dist/vite-plugins/built-url.d.ts.map +1 -1
  135. package/dist/vite-plugins/built-url.js +37 -24
  136. package/dist/vite-plugins/built-url.js.map +1 -1
  137. package/dist/vite-plugins/icons.d.ts +2 -1
  138. package/dist/vite-plugins/icons.d.ts.map +1 -1
  139. package/dist/vite-plugins/icons.js +25 -12
  140. package/dist/vite-plugins/icons.js.map +1 -1
  141. package/dist/vite-plugins/index.d.ts +2 -1
  142. package/dist/vite-plugins/index.d.ts.map +1 -1
  143. package/dist/vite-plugins/index.js +7 -5
  144. package/dist/vite-plugins/index.js.map +1 -1
  145. package/dist/vite-plugins/native.d.ts.map +1 -1
  146. package/dist/vite-plugins/native.js +23 -4
  147. package/dist/vite-plugins/native.js.map +1 -1
  148. package/dist/vite-plugins/resource-init-module.d.ts +7 -3
  149. package/dist/vite-plugins/resource-init-module.d.ts.map +1 -1
  150. package/dist/vite-plugins/resource-init-module.js +39 -14
  151. package/dist/vite-plugins/resource-init-module.js.map +1 -1
  152. package/dist/vite-plugins/resource-shared.d.ts +3 -1
  153. package/dist/vite-plugins/resource-shared.d.ts.map +1 -1
  154. package/dist/vite-plugins/resource-shared.js +3 -1
  155. package/dist/vite-plugins/resource-shared.js.map +1 -1
  156. package/dist/vite-plugins/resources.d.ts +1 -1
  157. package/dist/vite-plugins/resources.d.ts.map +1 -1
  158. package/dist/vite-plugins/resources.js +922 -86
  159. package/dist/vite-plugins/resources.js.map +1 -1
  160. package/dist/vite-plugins/settings-worker-env.d.ts.map +1 -1
  161. package/dist/vite-plugins/settings-worker-env.js +6 -3
  162. package/dist/vite-plugins/settings-worker-env.js.map +1 -1
  163. package/dist/vite-plugins/settings.d.ts +3 -1
  164. package/dist/vite-plugins/settings.d.ts.map +1 -1
  165. package/dist/vite-plugins/settings.js +63 -23
  166. package/dist/vite-plugins/settings.js.map +1 -1
  167. package/env.d.ts +11 -192
  168. package/package.json +10 -10
  169. package/src/builder.ts +6 -7
  170. package/src/codegen/run-codegen.ts +10 -7
  171. package/src/commands/build.ts +0 -5
  172. package/src/commands/codegen.ts +1 -3
  173. package/src/deploy/depends.ts +47 -18
  174. package/src/deploy/nfpm/config.ts +1 -1
  175. package/src/deploy/node-runtime/elf.ts +3 -3
  176. package/src/deploy/node-runtime/index.ts +2 -2
  177. package/src/deploy/notices/collect.ts +4 -2
  178. package/src/deploy/notices/packages.ts +5 -43
  179. package/src/deploy/payload/icons.ts +64 -34
  180. package/src/deploy/payload/stage.ts +5 -2
  181. package/src/deploy/run-deploy.ts +87 -12
  182. package/src/deploy/settings/build-manifest.ts +122 -0
  183. package/src/deploy/settings/index.ts +9 -4
  184. package/src/deploy/settings/libraries.ts +57 -0
  185. package/src/deploy/settings/paths.ts +10 -47
  186. package/src/deploy/targets/appimage.ts +32 -15
  187. package/src/deploy/targets/flatpak-manifest.ts +7 -1
  188. package/src/deploy/targets/flatpak-source.ts +9 -10
  189. package/src/deploy/types.ts +4 -4
  190. package/src/dev/icon-dir.ts +31 -7
  191. package/src/dev/runner-deps.ts +5 -1
  192. package/src/dev/runner-main.ts +4 -4
  193. package/src/dev/runner.ts +2 -2
  194. package/src/dev/schema-dir.ts +3 -1
  195. package/src/internal/build-manifest.ts +32 -0
  196. package/src/internal/icon-path.ts +127 -0
  197. package/src/internal/source-imports.ts +200 -0
  198. package/src/mcp/widget-properties.ts +3 -14
  199. package/src/settings/render.ts +99 -21
  200. package/src/settings/schema.ts +190 -17
  201. package/src/vite-plugins/asset-emitter.ts +1 -1
  202. package/src/vite-plugins/asset-extensions.ts +1 -0
  203. package/src/vite-plugins/asset-imports.ts +60 -19
  204. package/src/vite-plugins/asset-specifier.ts +73 -1
  205. package/src/vite-plugins/{bundled-packages.ts → build-manifest.ts} +30 -19
  206. package/src/vite-plugins/built-url.ts +49 -28
  207. package/src/vite-plugins/icons.ts +33 -14
  208. package/src/vite-plugins/index.ts +12 -5
  209. package/src/vite-plugins/native.ts +31 -4
  210. package/src/vite-plugins/resource-init-module.ts +44 -15
  211. package/src/vite-plugins/resource-shared.ts +4 -0
  212. package/src/vite-plugins/resources.ts +1459 -100
  213. package/src/vite-plugins/settings-worker-env.ts +7 -3
  214. package/src/vite-plugins/settings.ts +89 -21
  215. package/dist/vite-plugins/bundled-packages.d.ts +0 -5
  216. package/dist/vite-plugins/bundled-packages.d.ts.map +0 -1
  217. package/dist/vite-plugins/bundled-packages.js.map +0 -1
@@ -1,53 +1,82 @@
1
- import { basename, join } from "node:path";
1
+ import { join } from "node:path";
2
2
  import type { DeploySettings, StagedFile } from "../types.js";
3
+ import { relativeIconPath } from "../../internal/icon-path.js";
3
4
  import { listFilesRecursive } from "../../internal/list-files.js";
4
5
  import { copyInto } from "./copy-tree.js";
5
6
 
7
+ type ApplicationIconVariant = {
8
+ effectivePixels: number;
9
+ scalable: boolean;
10
+ };
11
+
6
12
  const ICON_EXTENSIONS = [".svg", ".png", ".xpm"];
7
13
  const SHARE_ICONS = "share/icons";
8
- const SCALABLE_DIR = "hicolor/scalable/apps";
9
- const SIZE_PATTERN = /^(?<width>[1-9]\d{0,3})x(?<height>[1-9]\d{0,3})$/;
10
- const SIZE_SEPARATORS = /[-_.]/;
11
-
12
- const isApplicationIcon = (settings: DeploySettings, rel: string): boolean =>
13
- ICON_EXTENSIONS.some((extension) => basename(rel) === `${settings.applicationId}${extension}`);
14
+ const SIZE_PATTERN = /^(?<width>[1-9]\d{0,3})x(?<height>[1-9]\d{0,3})(?:@(?<scale>[1-9]\d*))?$/;
14
15
 
15
- const getSquareSize = (token: string): number | null => {
16
+ const getSquareSize = (token: string): { pixels: number; scale: number } | null => {
16
17
  const groups = SIZE_PATTERN.exec(token)?.groups;
17
18
 
18
19
  if (groups === undefined || groups.width !== groups.height) {
19
20
  return null;
20
21
  }
21
22
 
22
- return Number(groups.width);
23
+ const pixels = Number(groups.width);
24
+ const scale = Number(groups.scale ?? "1");
25
+
26
+ return Number.isSafeInteger(pixels * scale) ? { pixels, scale } : null;
27
+ };
28
+
29
+ const isIconThemeSize = (segment: string | undefined): boolean =>
30
+ segment === "scalable" || segment === "symbolic" ||
31
+ (segment !== undefined && getSquareSize(segment) !== null);
32
+
33
+ const iconExtension = (applicationId: string, filename: string | undefined): string | undefined =>
34
+ ICON_EXTENSIONS.find((candidate) => filename === `${applicationId}${candidate}`);
35
+
36
+ const isApplicationIconLayout = (segments: (string | undefined)[]): boolean => {
37
+ const [share, icons, theme, size, context] = segments;
38
+
39
+ return share === "share" &&
40
+ icons === "icons" &&
41
+ theme === "hicolor" &&
42
+ context === "apps" &&
43
+ isIconThemeSize(size);
23
44
  };
24
45
 
25
- const firstSquareSize = (tokens: string[]): number | null =>
26
- tokens.map((token) => getSquareSize(token)).find((size) => size !== null) ?? null;
46
+ const classifyApplicationIcon = (applicationId: string, rel: string): ApplicationIconVariant | null => {
47
+ const segments = rel.split(/[\\/]/);
48
+
49
+ if (segments.length !== 6) {
50
+ return null;
51
+ }
27
52
 
28
- const getIconSize = (rel: string): number | null => firstSquareSize(rel.split("/").slice(0, -1));
53
+ const size = segments[3];
54
+ const filename = segments[5];
55
+ const extension = iconExtension(applicationId, filename);
29
56
 
30
- const themeDirFor = (file: string): string => {
31
- if (file.endsWith(".svg")) {
32
- return SCALABLE_DIR;
57
+ if (extension === undefined || !isApplicationIconLayout(segments)) {
58
+ return null;
33
59
  }
34
60
 
35
- const size = firstSquareSize(basename(file).split(SIZE_SEPARATORS));
61
+ const square = size === undefined ? null : getSquareSize(size);
36
62
 
37
- return size === null ? SCALABLE_DIR : `hicolor/${String(size)}x${String(size)}/apps`;
63
+ return {
64
+ effectivePixels: square === null ? 0 : square.pixels * square.scale,
65
+ scalable: extension === ".svg",
66
+ };
38
67
  };
39
68
 
40
- const stageIconTree = (root: string, iconsDir: string): StagedFile[] =>
41
- listFilesRecursive(iconsDir).map((file) => copyInto(root, join(SHARE_ICONS, file.rel), file.absPath));
69
+ const isApplicationIcon = (settings: DeploySettings, rel: string): boolean =>
70
+ classifyApplicationIcon(settings.applicationId, rel) !== null;
42
71
 
43
- const iconPathFor = (settings: DeploySettings, iconFile: string): string => {
44
- const extension = ICON_EXTENSIONS.find((candidate) => iconFile.endsWith(candidate)) ?? ".png";
72
+ const stageIconTree = (root: string, themePath: string): StagedFile[] =>
73
+ listFilesRecursive(themePath).map((file) => copyInto(root, join(SHARE_ICONS, file.rel), file.absPath));
45
74
 
46
- return join(themeDirFor(iconFile), `${settings.applicationId}${extension}`);
47
- };
75
+ const iconPathFor = (settings: DeploySettings, iconPath: string): string =>
76
+ relativeIconPath(settings.applicationId, iconPath);
48
77
 
49
- const stageIconFile = (settings: DeploySettings, root: string, iconFile: string): StagedFile[] =>
50
- [copyInto(root, join(SHARE_ICONS, iconPathFor(settings, iconFile)), iconFile)];
78
+ const stageIconFile = (settings: DeploySettings, root: string, iconPath: string): StagedFile[] =>
79
+ [copyInto(root, join(SHARE_ICONS, iconPathFor(settings, iconPath)), iconPath)];
51
80
 
52
81
  const assertApplicationIcon = (settings: DeploySettings, staged: StagedFile[]): void => {
53
82
  if (staged.some((file) => isApplicationIcon(settings, file.rel))) {
@@ -55,26 +84,27 @@ const assertApplicationIcon = (settings: DeploySettings, staged: StagedFile[]):
55
84
  }
56
85
 
57
86
  throw new Error(
58
- `Cannot deploy without an application icon: no ${settings.applicationId}.svg or ` +
59
- `${settings.applicationId}.png under ${settings.paths.iconsDir ?? "the project's icon directory"}. ` +
87
+ "Cannot deploy without a usable application icon: the configured theme must contain " +
88
+ `${settings.applicationId}.svg, ${settings.applicationId}.png, or ${settings.applicationId}.xpm ` +
89
+ "under hicolor/<size>/apps. " +
60
90
  "The desktop entry names " +
61
91
  `"${settings.applicationId}" as its icon, so the file has to match. ` +
62
- `Add ${settings.paths.dataDir ?? "data"}/icons/hicolor/scalable/apps/${settings.applicationId}.svg, ` +
63
- "or point `deploy.icons` at one.",
92
+ "Set `applicationIcon` to a theme directory containing " +
93
+ `hicolor/scalable/apps/${settings.applicationId}.svg, or point it at a single icon file.`,
64
94
  );
65
95
  };
66
96
 
67
97
  const stageIcons = (settings: DeploySettings, root: string): StagedFile[] => {
68
- const { iconsDir, iconFile } = settings.paths;
98
+ const source = settings.paths.applicationIcon;
69
99
 
70
- if (iconFile !== null) {
71
- return stageIconFile(settings, root, iconFile);
100
+ if (source.kind === "file") {
101
+ return stageIconFile(settings, root, source.path);
72
102
  }
73
103
 
74
- const staged = iconsDir === null ? [] : stageIconTree(root, iconsDir);
104
+ const staged = source.kind === "theme" ? stageIconTree(root, source.path) : [];
75
105
  assertApplicationIcon(settings, staged);
76
106
 
77
107
  return staged;
78
108
  };
79
109
 
80
- export { getIconSize, iconPathFor, stageIcons };
110
+ export { classifyApplicationIcon, iconPathFor, stageIcons, type ApplicationIconVariant };
@@ -2,8 +2,8 @@ import { sortStringsBy } from "@gtkx/utils";
2
2
  import { existsSync, rmSync } from "node:fs";
3
3
  import { join, resolve } from "node:path";
4
4
  import type { DeploySettings, DeployTargetName, NodeRuntime, NoticeSection, StagedFile } from "../types.js";
5
+ import { BUILD_MANIFEST_FILENAME } from "../../internal/build-manifest.js";
5
6
  import { listFilesRecursive } from "../../internal/list-files.js";
6
- import { BUNDLED_PACKAGES_FILENAME } from "../../vite-plugins/bundled-packages.js";
7
7
  import { BUNDLE_FILENAME } from "../../vite-plugins/esm-extension.js";
8
8
  import { renderCopyright } from "../freedesktop/copyright.js";
9
9
  import { renderDbusService } from "../freedesktop/dbus-service.js";
@@ -27,6 +27,7 @@ type StagedMetadata = {
27
27
  };
28
28
 
29
29
  const DIST_ICONS_DIR = "icons";
30
+ const LEGACY_PACKAGES_MANIFEST_FILENAME = "gtkx-packages.json";
30
31
  const NOTICES_FILENAME = "THIRD-PARTY-NOTICES";
31
32
  const NODE_LICENSE_FILENAME = "LICENSE";
32
33
  const SHARE_APPLICATIONS = "share/applications";
@@ -48,7 +49,9 @@ const licenseDirFor = (settings: DeploySettings): string => `${SHARE_LICENSES}/$
48
49
  const licenseDestination = (settings: DeploySettings): string => `${licenseDirFor(settings)}/LICENSE`;
49
50
  const noticesDestination = (settings: DeploySettings): string => `${licenseDirFor(settings)}/${NOTICES_FILENAME}`;
50
51
  const isIconAsset = (rel: string): boolean => rel === DIST_ICONS_DIR || rel.startsWith(`${DIST_ICONS_DIR}/`);
51
- const isBuildMetadata = (rel: string): boolean => rel === BUNDLED_PACKAGES_FILENAME;
52
+
53
+ const isBuildMetadata = (rel: string): boolean =>
54
+ rel === BUILD_MANIFEST_FILENAME || rel === LEGACY_PACKAGES_MANIFEST_FILENAME;
52
55
 
53
56
  const nodeLicenseDestination = (settings: DeploySettings): string =>
54
57
  `${licenseDirFor(settings)}/${NODE_FILENAME}/${NODE_LICENSE_FILENAME}`;
@@ -20,10 +20,11 @@ import { resolveNodeRuntime } from "./node-runtime/index.js";
20
20
  import { collectNotices } from "./notices/collect.js";
21
21
  import { type StagedMetadata, stageOverlays, stagePayload } from "./payload/stage.js";
22
22
  import { DEFAULT_TARGETS, parseTargetList, targetsFor } from "./registry.js";
23
+ import { readBuildManifest } from "./settings/build-manifest.js";
23
24
  import { resolveDeploySettings } from "./settings/index.js";
24
25
  import { readPackageManifest } from "./settings/package-manifest.js";
25
26
  import { missingDeployError } from "./settings/starter.js";
26
- import { finishArgsFor, hasDisplaySocket } from "./targets/flatpak-manifest.js";
27
+ import { finishArgsFor, hasDisplaySocket, runtimeLabelFor } from "./targets/flatpak-manifest.js";
27
28
  import { detectPackageManager } from "./targets/flatpak-sources.js";
28
29
  import {
29
30
  APPSTREAMCLI,
@@ -50,6 +51,8 @@ const BUILD_MODE = "production";
50
51
  const BYTES_PER_MIB = 1024 * 1024;
51
52
  const WEBKIT_LIBRARY = "WebKit-6.0";
52
53
  const NETWORK_ARG = "--share=network";
54
+ const APPIMAGE_TARGET = "appimage";
55
+ const FLATPAK_TARGET = "flatpak";
53
56
 
54
57
  const displayPath = (settings: DeploySettings, path: string): string => relative(settings.paths.root, path);
55
58
  const megabytes = (size: number): string => (size / BYTES_PER_MIB).toFixed(1);
@@ -66,7 +69,7 @@ const renderMetadata = (settings: DeploySettings): StagedMetadata => ({
66
69
  });
67
70
 
68
71
  const isFlathubSubmission = (settings: DeploySettings, targets: DeployTarget[]): boolean =>
69
- settings.deploy.flatpak?.mode === "source" && targets.some((target) => target.name === "flatpak");
72
+ settings.deploy.flatpak?.mode === "source" && targets.some((target) => target.name === FLATPAK_TARGET);
70
73
 
71
74
  const validateMetadata = (settings: DeploySettings, metadata: StagedMetadata, areWarningsFatal: boolean): void => {
72
75
  const dir = settings.paths.metadata;
@@ -104,7 +107,7 @@ const warnMissingNetwork = (settings: DeploySettings, finishArgs: string[]): voi
104
107
  };
105
108
 
106
109
  const warnFlatpakPermissions = (targets: DeployTarget[], settings: DeploySettings): void => {
107
- if (targets.every((target) => target.name !== "flatpak")) {
110
+ if (targets.every((target) => target.name !== FLATPAK_TARGET)) {
108
111
  return;
109
112
  }
110
113
 
@@ -113,10 +116,68 @@ const warnFlatpakPermissions = (targets: DeployTarget[], settings: DeploySetting
113
116
  warnMissingNetwork(settings, finishArgs);
114
117
  };
115
118
 
119
+ const minimumSummary = (settings: DeploySettings): string =>
120
+ settings.libraries
121
+ .filter((library) => settings.minimumLibraryVersions[library] !== undefined)
122
+ .map((library) => `${library} >= ${String(settings.minimumLibraryVersions[library])}`)
123
+ .join(", ");
124
+
125
+ const warnAppImageMinimums = (settings: DeploySettings, summary: string): void => {
126
+ warn(
127
+ `An AppImage cannot declare a dependency, so nothing stops ${settings.name} from starting on a host ` +
128
+ `whose libraries are older than the ones its bindings were generated against (${summary}). ` +
129
+ "Publish that requirement alongside the AppImage, or build on the oldest host you support.",
130
+ );
131
+ };
132
+
133
+ const warnFlatpakMinimums = (settings: DeploySettings, summary: string): void => {
134
+ warn(
135
+ `The flatpak bundles binaries built against ${summary}, but runs them on ` +
136
+ `${runtimeLabelFor(settings)}, whose libraries are whatever that runtime ships. ` +
137
+ 'Set `deploy.flatpak.mode: "source"` to build inside the runtime instead.',
138
+ );
139
+ };
140
+
141
+ const warnUnusedMinimums = (settings: DeploySettings): void => {
142
+ const minimums = settings.deploy.minimumLibraryVersions;
143
+
144
+ if (minimums === undefined) {
145
+ return;
146
+ }
147
+
148
+ const unused = Object.keys(minimums).filter((library) => !settings.libraries.includes(library));
149
+
150
+ if (unused.length === 0) {
151
+ return;
152
+ }
153
+
154
+ warn(
155
+ `\`deploy.minimumLibraryVersions\` names ${unused.join(", ")}, which this project generates no bindings for, ` +
156
+ "so those minimums change nothing.",
157
+ );
158
+ };
159
+
160
+ const isPrebuiltFlatpak = (targets: DeployTarget[], settings: DeploySettings): boolean =>
161
+ targets.some((target) => target.name === FLATPAK_TARGET) && settings.deploy.flatpak?.mode !== "source";
162
+
163
+ const warnLibraryMinimums = (targets: DeployTarget[], settings: DeploySettings): void => {
164
+ const summary = minimumSummary(settings);
165
+
166
+ if (summary.length > 0 && targets.some((target) => target.name === APPIMAGE_TARGET)) {
167
+ warnAppImageMinimums(settings, summary);
168
+ }
169
+
170
+ if (summary.length > 0 && isPrebuiltFlatpak(targets, settings)) {
171
+ warnFlatpakMinimums(settings, summary);
172
+ }
173
+
174
+ warnUnusedMinimums(settings);
175
+ };
176
+
116
177
  const sourceModeTools = (targets: DeployTarget[], settings: DeploySettings): DeployTool[] => {
117
178
  const isFlatpakSource = settings.deploy.flatpak?.mode === "source";
118
179
 
119
- if (!isFlatpakSource || targets.every((target) => target.name !== "flatpak")) {
180
+ if (!isFlatpakSource || targets.every((target) => target.name !== FLATPAK_TARGET)) {
120
181
  return [];
121
182
  }
122
183
 
@@ -124,7 +185,7 @@ const sourceModeTools = (targets: DeployTarget[], settings: DeploySettings): Dep
124
185
  };
125
186
 
126
187
  const isNodeRequired = (targets: DeployTarget[], settings: DeploySettings): boolean =>
127
- !(settings.deploy.flatpak?.mode === "source" && targets.every((target) => target.name === "flatpak"));
188
+ !(settings.deploy.flatpak?.mode === "source" && targets.every((target) => target.name === FLATPAK_TARGET));
128
189
 
129
190
  const runtimeToolsFor = (targets: DeployTarget[], settings: DeploySettings): DeployTool[] => {
130
191
  if (!isNodeRequired(targets, settings)) {
@@ -147,6 +208,7 @@ const preflight = (targets: DeployTarget[], settings: DeploySettings, shouldPrin
147
208
  assertTools(report);
148
209
  warnMissingOptional(report);
149
210
  warnFlatpakPermissions(targets, settings);
211
+ warnLibraryMinimums(targets, settings);
150
212
  };
151
213
 
152
214
  const resolveTargetNames = (options: DeployOptions, settings: DeploySettings): string[] => {
@@ -180,15 +242,28 @@ const buildPayload = async (
180
242
  await buildApp({ entry: options.entry, vite: { root: options.cwd } });
181
243
  }
182
244
 
183
- const node = options.shouldPrintManifests || !isNodeRequired(targets, settings)
184
- ? null
185
- : await resolveNodeRuntime(settings);
245
+ const buildManifest = readBuildManifest(settings);
186
246
 
187
- const stage = stagePayload({ settings, node, metadata });
188
- info(`Staged ${String(stage.length)} files into ${displayPath(settings, settings.paths.stage)}`);
189
- const notices = collectNotices({ settings, node });
247
+ const builtSettings: DeploySettings = {
248
+ ...settings,
249
+ paths: { ...settings.paths, schemaFiles: buildManifest.schemaFiles },
250
+ };
190
251
 
191
- return { settings, node, stage, notices, overlays: stageOverlays(settings, notices) };
252
+ const node = options.shouldPrintManifests || !isNodeRequired(targets, builtSettings)
253
+ ? null
254
+ : await resolveNodeRuntime(builtSettings);
255
+
256
+ const stage = stagePayload({ settings: builtSettings, node, metadata });
257
+ info(`Staged ${String(stage.length)} files into ${displayPath(builtSettings, builtSettings.paths.stage)}`);
258
+ const notices = collectNotices({ settings: builtSettings, node, packages: buildManifest.packages });
259
+
260
+ return {
261
+ settings: builtSettings,
262
+ node,
263
+ stage,
264
+ notices,
265
+ overlays: stageOverlays(builtSettings, notices),
266
+ };
192
267
  };
193
268
 
194
269
  const renderTargetManifests = (
@@ -0,0 +1,122 @@
1
+ import { isRecord, sortStrings } from "@gtkx/utils";
2
+ import { existsSync, readFileSync, statSync } from "node:fs";
3
+ import { isAbsolute, join, relative, resolve, sep } from "node:path";
4
+ import type { DeploySettings } from "../types.js";
5
+ import {
6
+ BUILD_MANIFEST_FILENAME,
7
+ BUILD_MANIFEST_FORMAT_VERSION,
8
+ BUILD_MANIFEST_GENERATOR,
9
+ type BuildManifest,
10
+ type RecordedPackage,
11
+ } from "../../internal/build-manifest.js";
12
+ import {
13
+ assertUniqueSchemaBasenames,
14
+ projectRelativeSchemaPath,
15
+ SCHEMA_SUFFIX,
16
+ } from "../../settings/schema.js";
17
+
18
+ type ResolvedBuildManifest = {
19
+ packages: RecordedPackage[];
20
+ schemaFiles: string[];
21
+ };
22
+
23
+ const invalidManifest = (path: string): Error =>
24
+ new Error(`Cannot deploy: ${path} is not a valid GTKX build manifest. Run \`gtkx build\` again.`);
25
+
26
+ const recordedSchemas = (value: unknown[], path: string): string[] =>
27
+ value.map((entry) => {
28
+ if (typeof entry !== "string") {
29
+ throw invalidManifest(path);
30
+ }
31
+
32
+ return entry;
33
+ });
34
+
35
+ const recordedPackage = (value: unknown, path: string): RecordedPackage => {
36
+ if (
37
+ !isRecord(value) ||
38
+ typeof value.name !== "string" ||
39
+ typeof value.dir !== "string" ||
40
+ isAbsolute(value.dir) ||
41
+ (value.version !== null && typeof value.version !== "string")
42
+ ) {
43
+ throw invalidManifest(path);
44
+ }
45
+
46
+ return { name: value.name, version: value.version, dir: value.dir };
47
+ };
48
+
49
+ const parseBuildManifest = (value: unknown, path: string): BuildManifest => {
50
+ if (
51
+ !isRecord(value) ||
52
+ value.generator !== BUILD_MANIFEST_GENERATOR ||
53
+ value.formatVersion !== BUILD_MANIFEST_FORMAT_VERSION ||
54
+ !Array.isArray(value.schemas) ||
55
+ !Array.isArray(value.packages)
56
+ ) {
57
+ throw invalidManifest(path);
58
+ }
59
+
60
+ const packages: unknown[] = value.packages;
61
+
62
+ return {
63
+ generator: BUILD_MANIFEST_GENERATOR,
64
+ formatVersion: BUILD_MANIFEST_FORMAT_VERSION,
65
+ schemas: recordedSchemas(value.schemas, path),
66
+ packages: packages.map((entry) => recordedPackage(entry, path)),
67
+ };
68
+ };
69
+
70
+ const isFile = (path: string): boolean => {
71
+ try {
72
+ return statSync(path, { throwIfNoEntry: false })?.isFile() === true;
73
+ } catch {
74
+ return false;
75
+ }
76
+ };
77
+
78
+ const isInsideProject = (root: string, filePath: string): boolean => {
79
+ const rel = relative(root, filePath);
80
+
81
+ return rel.length > 0 && rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
82
+ };
83
+
84
+ const resolveRecordedSchema = (root: string, manifestPath: string, entry: string): string => {
85
+ if (isAbsolute(entry) || !entry.endsWith(SCHEMA_SUFFIX)) {
86
+ throw new Error(`Cannot deploy: ${manifestPath} records an invalid GSettings schema path.`);
87
+ }
88
+
89
+ const filePath = resolve(root, entry);
90
+
91
+ if (!isInsideProject(root, filePath)) {
92
+ throw new Error(`Cannot deploy: ${manifestPath} records a GSettings schema outside the project.`);
93
+ }
94
+
95
+ if (!isFile(filePath)) {
96
+ throw new Error(`Cannot deploy: the recorded GSettings schema ${filePath} is missing.`);
97
+ }
98
+
99
+ if (projectRelativeSchemaPath(root, filePath) === null) {
100
+ throw new Error(`Cannot deploy: ${manifestPath} records a GSettings schema outside the project.`);
101
+ }
102
+
103
+ return filePath;
104
+ };
105
+
106
+ const readBuildManifest = (settings: DeploySettings): ResolvedBuildManifest => {
107
+ const manifestPath = join(settings.paths.dist, BUILD_MANIFEST_FILENAME);
108
+
109
+ if (!existsSync(manifestPath)) {
110
+ throw new Error(`Cannot deploy: ${manifestPath} is missing. Run \`gtkx build\` first.`);
111
+ }
112
+
113
+ const parsed: unknown = JSON.parse(readFileSync(manifestPath, "utf8"));
114
+ const manifest = parseBuildManifest(parsed, manifestPath);
115
+ const files = manifest.schemas.map((entry) => resolveRecordedSchema(settings.paths.root, manifestPath, entry));
116
+ const unique = sortStrings(new Set(files));
117
+ assertUniqueSchemaBasenames(unique);
118
+
119
+ return { packages: manifest.packages, schemaFiles: unique };
120
+ };
121
+
122
+ export { readBuildManifest };
@@ -22,6 +22,7 @@ import {
22
22
  resolveSummary,
23
23
  resolveVersionString,
24
24
  } from "./identity.js";
25
+ import { resolveLibraries } from "./libraries.js";
25
26
  import { readPackageManifest } from "./package-manifest.js";
26
27
  import { resolvePaths } from "./paths.js";
27
28
  import { resolveScreenshots } from "./screenshots.js";
@@ -113,8 +114,6 @@ const resolveDesktopActions = (deploy: DeployConfig): DeployDesktopAction[] =>
113
114
  icon: action.icon ?? null,
114
115
  }));
115
116
 
116
- const resolveLibraries = (config: Config): string[] => (Array.isArray(config.libraries) ? config.libraries : []);
117
-
118
117
  const identitySlice = ({ identity, developer, date }: ResolvedCore): IdentitySlice => ({
119
118
  applicationId: identity.applicationId,
120
119
  binaryName: resolveBinaryName(identity),
@@ -181,8 +180,14 @@ const resolveDeploySettings = (request: SettingsRequest): DeploySettings => {
181
180
  extraFiles: resolveExtraFiles(deploy),
182
181
  versions: core.versions,
183
182
  arch: resolveArch(),
184
- paths: resolvePaths({ root: request.root, deploy, outDirOverride: request.outDirOverride }),
185
- libraries: resolveLibraries(request.config),
183
+ paths: resolvePaths({
184
+ root: request.root,
185
+ deploy,
186
+ applicationIcon: request.config.applicationIcon,
187
+ applicationId: request.config.applicationId,
188
+ outDirOverride: request.outDirOverride,
189
+ }),
190
+ ...resolveLibraries(request.root, request.config),
186
191
  deploy,
187
192
  };
188
193
  };
@@ -0,0 +1,57 @@
1
+ import type { Config } from "@gtkx/config";
2
+ import {
3
+ resolveLibraries as expandLibraries,
4
+ type GeneratedLibraries,
5
+ readGeneratedLibraries,
6
+ resolveGirPath,
7
+ resolveStore,
8
+ } from "@gtkx/codegen";
9
+
10
+ type ResolvedLibraries = {
11
+ libraries: string[];
12
+ minimumLibraryVersions: Record<string, string>;
13
+ };
14
+
15
+ const DEFAULT_LIBRARY = "Gtk-4.0";
16
+ const DEFAULT_LIBRARY_PREFIX = "Gtk-";
17
+
18
+ const generatedLibraries = (root: string): GeneratedLibraries | null => {
19
+ try {
20
+ return readGeneratedLibraries(resolveStore(root).gi.storeDir);
21
+ } catch {
22
+ return null;
23
+ }
24
+ };
25
+
26
+ const discoveredLibraries = (config: Config): string[] | null => {
27
+ try {
28
+ return expandLibraries(config.libraries, resolveGirPath(config.girPath));
29
+ } catch {
30
+ return null;
31
+ }
32
+ };
33
+
34
+ const configLibraries = (config: Config): string[] => {
35
+ const libraries = config.libraries;
36
+
37
+ if (!Array.isArray(libraries)) {
38
+ return discoveredLibraries(config) ?? [DEFAULT_LIBRARY];
39
+ }
40
+
41
+ return libraries.some((library) => library.startsWith(DEFAULT_LIBRARY_PREFIX))
42
+ ? [...libraries]
43
+ : [DEFAULT_LIBRARY, ...libraries];
44
+ };
45
+
46
+ const resolveLibraries = (root: string, config: Config): ResolvedLibraries => {
47
+ const generated = generatedLibraries(root);
48
+ const libraries = generated?.libraries ?? configLibraries(config);
49
+ const deploy = config.deploy ?? {};
50
+
51
+ return {
52
+ libraries,
53
+ minimumLibraryVersions: deploy.minimumLibraryVersions ?? {},
54
+ };
55
+ };
56
+
57
+ export { resolveLibraries };
@@ -1,23 +1,19 @@
1
- import { type Stats, statSync } from "node:fs";
1
+ import type { Config } from "@gtkx/config";
2
+ import { statSync } from "node:fs";
2
3
  import { isAbsolute, join, relative, resolve } from "node:path";
3
4
  import type { DeployConfig, DeployPaths } from "../types.js";
4
- import { resolveDataDir } from "../../internal/data-dir.js";
5
- import { findSchemaFiles } from "../../settings/schema.js";
5
+ import { resolveApplicationIcon } from "../../internal/icon-path.js";
6
6
 
7
7
  type PathsRequest = {
8
8
  root: string;
9
9
  deploy: DeployConfig;
10
+ applicationIcon: Config["applicationIcon"];
11
+ applicationId: Config["applicationId"];
10
12
  outDirOverride: string | undefined;
11
13
  };
12
14
 
13
- type ResolvedIcons = {
14
- iconsDir: string | null;
15
- iconFile: string | null;
16
- };
17
-
18
15
  const DEFAULT_OUT_DIR = "build";
19
16
  const DIST_DIR = "dist";
20
- const ICONS_DIR = "icons";
21
17
  const LICENSE_CANDIDATES = ["LICENSE", "LICENSE.md", "LICENSE.txt", "COPYING", "COPYING.md"];
22
18
 
23
19
  const isInside = (parent: string, candidate: string): boolean => {
@@ -37,17 +33,14 @@ const resolveOutDir = ({ root, deploy, outDirOverride }: PathsRequest): string =
37
33
  return outDir;
38
34
  };
39
35
 
40
- const getStats = (path: string): Stats | undefined => {
36
+ const existingFile = (path: string): string | null => {
41
37
  try {
42
- return statSync(path, { throwIfNoEntry: false });
38
+ return statSync(path, { throwIfNoEntry: false })?.isFile() === true ? path : null;
43
39
  } catch {
44
- return undefined;
40
+ return null;
45
41
  }
46
42
  };
47
43
 
48
- const existingFile = (path: string): string | null => (getStats(path)?.isFile() === true ? path : null);
49
- const existingDir = (path: string): string | null => (getStats(path)?.isDirectory() === true ? path : null);
50
-
51
44
  const resolveLicenseFile = (root: string, configured: string | undefined): string | null => {
52
45
  if (configured !== undefined) {
53
46
  const path = resolve(root, configured);
@@ -62,37 +55,9 @@ const resolveLicenseFile = (root: string, configured: string | undefined): strin
62
55
  return LICENSE_CANDIDATES.map((name) => existingFile(join(root, name))).find((path) => path !== null) ?? null;
63
56
  };
64
57
 
65
- const configuredIcons = (root: string, configured: string): ResolvedIcons => {
66
- const path = resolve(root, configured);
67
-
68
- if (!isInside(root, path)) {
69
- throw new Error(`Cannot use "${configured}" as the icon path: it is outside ${root}`);
70
- }
71
-
72
- if (existingDir(path) !== null) {
73
- return { iconsDir: path, iconFile: null };
74
- }
75
-
76
- if (existingFile(path) === null) {
77
- throw new Error(`Cannot read the icon path "${configured}": no such file or directory under ${root}`);
78
- }
79
-
80
- return { iconsDir: null, iconFile: path };
81
- };
82
-
83
- const defaultIcons = (root: string, dataDir: string | null): ResolvedIcons => ({
84
- iconsDir: dataDir === null ? null : existingDir(join(root, dataDir, ICONS_DIR)),
85
- iconFile: null,
86
- });
87
-
88
- const resolveIcons = (root: string, dataDir: string | null, configured: string | undefined): ResolvedIcons =>
89
- configured === undefined ? defaultIcons(root, dataDir) : configuredIcons(root, configured);
90
-
91
58
  const resolvePaths = (request: PathsRequest): DeployPaths => {
92
59
  const { root, deploy } = request;
93
60
  const outDir = resolveOutDir(request);
94
- const dataDir = resolveDataDir(root);
95
- const { iconsDir, iconFile } = resolveIcons(root, dataDir, deploy.icons);
96
61
 
97
62
  return {
98
63
  root,
@@ -104,11 +69,9 @@ const resolvePaths = (request: PathsRequest): DeployPaths => {
104
69
  overlay: join(outDir, "overlay"),
105
70
  targets: join(outDir, "targets"),
106
71
  output: join(outDir, "out"),
107
- dataDir,
108
- iconsDir,
109
- iconFile,
72
+ applicationIcon: resolveApplicationIcon(root, request.applicationId, request.applicationIcon),
110
73
  licenseFile: resolveLicenseFile(root, deploy.licenseFile),
111
- schemaFiles: dataDir === null ? [] : findSchemaFiles(join(root, dataDir)),
74
+ schemaFiles: [],
112
75
  };
113
76
  };
114
77