@gtkx/cli 1.2.0 → 1.2.2

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 (117) hide show
  1. package/dist/builder.d.ts.map +1 -1
  2. package/dist/builder.js +2 -0
  3. package/dist/builder.js.map +1 -1
  4. package/dist/deploy/freedesktop/copyright.d.ts +2 -2
  5. package/dist/deploy/freedesktop/copyright.d.ts.map +1 -1
  6. package/dist/deploy/freedesktop/copyright.js +105 -14
  7. package/dist/deploy/freedesktop/copyright.js.map +1 -1
  8. package/dist/deploy/node-runtime/download.d.ts +5 -1
  9. package/dist/deploy/node-runtime/download.d.ts.map +1 -1
  10. package/dist/deploy/node-runtime/download.js +6 -3
  11. package/dist/deploy/node-runtime/download.js.map +1 -1
  12. package/dist/deploy/node-runtime/index.d.ts +2 -1
  13. package/dist/deploy/node-runtime/index.d.ts.map +1 -1
  14. package/dist/deploy/node-runtime/index.js +10 -9
  15. package/dist/deploy/node-runtime/index.js.map +1 -1
  16. package/dist/deploy/node-runtime/license.d.ts +4 -0
  17. package/dist/deploy/node-runtime/license.d.ts.map +1 -0
  18. package/dist/deploy/node-runtime/license.js +19 -0
  19. package/dist/deploy/node-runtime/license.js.map +1 -0
  20. package/dist/deploy/notices/collect.d.ts +8 -0
  21. package/dist/deploy/notices/collect.d.ts.map +1 -0
  22. package/dist/deploy/notices/collect.js +17 -0
  23. package/dist/deploy/notices/collect.js.map +1 -0
  24. package/dist/deploy/notices/dependencies.d.ts +5 -0
  25. package/dist/deploy/notices/dependencies.d.ts.map +1 -0
  26. package/dist/deploy/notices/dependencies.js +39 -0
  27. package/dist/deploy/notices/dependencies.js.map +1 -0
  28. package/dist/deploy/notices/gtkx.d.ts +6 -0
  29. package/dist/deploy/notices/gtkx.d.ts.map +1 -0
  30. package/dist/deploy/notices/gtkx.js +68 -0
  31. package/dist/deploy/notices/gtkx.js.map +1 -0
  32. package/dist/deploy/notices/libraries.d.ts +4 -0
  33. package/dist/deploy/notices/libraries.d.ts.map +1 -0
  34. package/dist/deploy/notices/libraries.js +60 -0
  35. package/dist/deploy/notices/libraries.js.map +1 -0
  36. package/dist/deploy/notices/node-runtime.d.ts +4 -0
  37. package/dist/deploy/notices/node-runtime.d.ts.map +1 -0
  38. package/dist/deploy/notices/node-runtime.js +78 -0
  39. package/dist/deploy/notices/node-runtime.js.map +1 -0
  40. package/dist/deploy/notices/packages.d.ts +13 -0
  41. package/dist/deploy/notices/packages.d.ts.map +1 -0
  42. package/dist/deploy/notices/packages.js +115 -0
  43. package/dist/deploy/notices/packages.js.map +1 -0
  44. package/dist/deploy/notices/render.d.ts +4 -0
  45. package/dist/deploy/notices/render.d.ts.map +1 -0
  46. package/dist/deploy/notices/render.js +41 -0
  47. package/dist/deploy/notices/render.js.map +1 -0
  48. package/dist/deploy/notices/text.d.ts +5 -0
  49. package/dist/deploy/notices/text.d.ts.map +1 -0
  50. package/dist/deploy/notices/text.js +34 -0
  51. package/dist/deploy/notices/text.js.map +1 -0
  52. package/dist/deploy/payload/copy-tree.d.ts +2 -1
  53. package/dist/deploy/payload/copy-tree.d.ts.map +1 -1
  54. package/dist/deploy/payload/copy-tree.js +3 -1
  55. package/dist/deploy/payload/copy-tree.js.map +1 -1
  56. package/dist/deploy/payload/stage.d.ts +7 -3
  57. package/dist/deploy/payload/stage.d.ts.map +1 -1
  58. package/dist/deploy/payload/stage.js +25 -14
  59. package/dist/deploy/payload/stage.js.map +1 -1
  60. package/dist/deploy/run-deploy.d.ts.map +1 -1
  61. package/dist/deploy/run-deploy.js +22 -5
  62. package/dist/deploy/run-deploy.js.map +1 -1
  63. package/dist/deploy/settings/extra-files.d.ts +4 -0
  64. package/dist/deploy/settings/extra-files.d.ts.map +1 -0
  65. package/dist/deploy/settings/extra-files.js +11 -0
  66. package/dist/deploy/settings/extra-files.js.map +1 -0
  67. package/dist/deploy/settings/index.d.ts.map +1 -1
  68. package/dist/deploy/settings/index.js +2 -1
  69. package/dist/deploy/settings/index.js.map +1 -1
  70. package/dist/deploy/settings/package-manifest.d.ts +1 -0
  71. package/dist/deploy/settings/package-manifest.d.ts.map +1 -1
  72. package/dist/deploy/settings/package-manifest.js +7 -0
  73. package/dist/deploy/settings/package-manifest.js.map +1 -1
  74. package/dist/deploy/settings/paths.d.ts +2 -1
  75. package/dist/deploy/settings/paths.d.ts.map +1 -1
  76. package/dist/deploy/settings/paths.js +1 -1
  77. package/dist/deploy/settings/paths.js.map +1 -1
  78. package/dist/deploy/targets/flatpak-manifest.d.ts +3 -1
  79. package/dist/deploy/targets/flatpak-manifest.d.ts.map +1 -1
  80. package/dist/deploy/targets/flatpak-manifest.js +50 -4
  81. package/dist/deploy/targets/flatpak-manifest.js.map +1 -1
  82. package/dist/deploy/targets/flatpak-source.d.ts.map +1 -1
  83. package/dist/deploy/targets/flatpak-source.js +55 -1
  84. package/dist/deploy/targets/flatpak-source.js.map +1 -1
  85. package/dist/deploy/types.d.ts +22 -2
  86. package/dist/deploy/types.d.ts.map +1 -1
  87. package/dist/deploy/types.js.map +1 -1
  88. package/dist/vite-plugins/bundled-packages.d.ts +5 -0
  89. package/dist/vite-plugins/bundled-packages.d.ts.map +1 -0
  90. package/dist/vite-plugins/bundled-packages.js +63 -0
  91. package/dist/vite-plugins/bundled-packages.js.map +1 -0
  92. package/package.json +9 -9
  93. package/src/builder.ts +2 -0
  94. package/src/deploy/freedesktop/copyright.ts +153 -18
  95. package/src/deploy/node-runtime/download.ts +13 -4
  96. package/src/deploy/node-runtime/index.ts +17 -11
  97. package/src/deploy/node-runtime/license.ts +24 -0
  98. package/src/deploy/notices/collect.ts +26 -0
  99. package/src/deploy/notices/dependencies.ts +53 -0
  100. package/src/deploy/notices/gtkx.ts +92 -0
  101. package/src/deploy/notices/libraries.ts +81 -0
  102. package/src/deploy/notices/node-runtime.ts +101 -0
  103. package/src/deploy/notices/packages.ts +168 -0
  104. package/src/deploy/notices/render.ts +52 -0
  105. package/src/deploy/notices/text.ts +42 -0
  106. package/src/deploy/payload/copy-tree.ts +5 -1
  107. package/src/deploy/payload/stage.ts +40 -16
  108. package/src/deploy/run-deploy.ts +27 -5
  109. package/src/deploy/settings/extra-files.ts +21 -0
  110. package/src/deploy/settings/index.ts +2 -1
  111. package/src/deploy/settings/package-manifest.ts +10 -0
  112. package/src/deploy/settings/paths.ts +1 -1
  113. package/src/deploy/targets/flatpak-manifest.ts +69 -4
  114. package/src/deploy/targets/flatpak-source.ts +87 -1
  115. package/src/deploy/types.ts +27 -1
  116. package/src/vite-plugins/bundled-packages.ts +95 -0
  117. package/LICENSE +0 -373
@@ -0,0 +1,101 @@
1
+ import { warn } from "@gtkx/utils";
2
+ import type { DeploySettings, NodeRuntime, NoticeSection } from "../types.js";
3
+ import { sourcePathFor } from "../node-runtime/index.js";
4
+ import { licenseBesideNode } from "../node-runtime/license.js";
5
+ import { NODE_FILENAME } from "../payload/launcher.js";
6
+ import { nodeLicenseDestination } from "../payload/stage.js";
7
+ import { copyrightLines, readLicenseText } from "./text.js";
8
+
9
+ const TITLE = "Node.js runtime";
10
+ const LICENSE_NAME = "Node.js";
11
+ const SOURCE_URL = "https://github.com/nodejs/node";
12
+ const LICENSE_URL = "https://github.com/nodejs/node/blob/main/LICENSE";
13
+
14
+ const SUMMARY = [
15
+ "This package carries the Node.js runtime that runs the application. Its license is the one Node.js",
16
+ "publishes as a single document, covering Node.js itself under the MIT terms together with the",
17
+ "components it embeds under theirs, among them V8, OpenSSL, ICU, libuv, zlib, brotli and llhttp.",
18
+ ];
19
+
20
+ const CARRIED_SUMMARY = ["The notice below is that document, reproduced from the release this package bundles."];
21
+
22
+ const MISSING_SUMMARY = [
23
+ "This run had no copy of that document at hand, so the notice below names it and it is published at",
24
+ `${LICENSE_URL}.`,
25
+ ];
26
+
27
+ const MISSING_LICENSE =
28
+ "Cannot read a license file for the Node.js runtime this deploy bundles. An official release unpacks it " +
29
+ 'next to the binary, so point `deploy.node.path` at one, or use `deploy.node.source: "download"`, and ' +
30
+ "the notices carry it.";
31
+
32
+ const isSourceMode = (settings: DeploySettings): boolean => settings.deploy.flatpak?.mode === "source";
33
+
34
+ const versionFor = (settings: DeploySettings, node: NodeRuntime | null): string =>
35
+ node?.version ?? settings.deploy.node?.version ?? process.versions.node;
36
+
37
+ const licenseFileFor = (settings: DeploySettings, node: NodeRuntime | null): string | null => {
38
+ if (node !== null) {
39
+ return node.licenseFile;
40
+ }
41
+
42
+ if ((settings.deploy.node?.source ?? "download") === "download") {
43
+ return null;
44
+ }
45
+
46
+ return licenseBesideNode(sourcePathFor(settings));
47
+ };
48
+
49
+ const textFor = (settings: DeploySettings, node: NodeRuntime | null): string | null => {
50
+ const licenseFile = licenseFileFor(settings, node);
51
+
52
+ return licenseFile === null ? null : readLicenseText(licenseFile);
53
+ };
54
+
55
+ const extensionSummary = (settings: DeploySettings): string[] => [
56
+ "The flatpak built from source copies its runtime out of the Node SDK extension in the build sandbox.",
57
+ `That build installs the license file the extension ships as ${nodeLicenseDestination(settings)}, and`,
58
+ "installs nothing when the extension ships none, so read the text at the address above if that file is",
59
+ "not in the package you received.",
60
+ ];
61
+
62
+ const carriedSummary = (settings: DeploySettings, text: string | null): string[] => {
63
+ if (isSourceMode(settings)) {
64
+ return [...MISSING_SUMMARY, ...extensionSummary(settings)];
65
+ }
66
+
67
+ return text === null ? MISSING_SUMMARY : CARRIED_SUMMARY;
68
+ };
69
+
70
+ const summaryFor = (settings: DeploySettings, text: string | null): string[] => [
71
+ ...SUMMARY,
72
+ ...carriedSummary(settings, text),
73
+ ];
74
+
75
+ const warnMissingLicense = (node: NodeRuntime | null, text: string | null): void => {
76
+ if (node === null || text !== null) {
77
+ return;
78
+ }
79
+
80
+ warn(MISSING_LICENSE);
81
+ };
82
+
83
+ const nodeNotices = (settings: DeploySettings, node: NodeRuntime | null): NoticeSection => {
84
+ const text = textFor(settings, node);
85
+ warnMissingLicense(node, text);
86
+
87
+ return {
88
+ title: TITLE,
89
+ files: [`lib/${settings.binaryName}/${NODE_FILENAME}`],
90
+ summary: summaryFor(settings, text),
91
+ notices: [{
92
+ subject: `Node.js ${versionFor(settings, node)}`,
93
+ license: LICENSE_NAME,
94
+ source: SOURCE_URL,
95
+ copyright: copyrightLines(text),
96
+ text,
97
+ }],
98
+ };
99
+ };
100
+
101
+ export { nodeNotices };
@@ -0,0 +1,168 @@
1
+ import { isRecord, sortStringsBy, warn } from "@gtkx/utils";
2
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
3
+ import { join, resolve } from "node:path";
4
+ import type { DeploySettings } from "../types.js";
5
+ import { BUNDLED_PACKAGES_FILENAME } from "../../vite-plugins/bundled-packages.js";
6
+ import { type PackageManifest, readPackageManifest } from "../settings/package-manifest.js";
7
+ import { copyrightLines, licenseTextIn } from "./text.js";
8
+
9
+ type RecordedPackage = {
10
+ name: string;
11
+ version: string | null;
12
+ dir: string;
13
+ };
14
+
15
+ type BundledPackage = {
16
+ name: string;
17
+ version: string | null;
18
+ license: string | null;
19
+ source: string | null;
20
+ copyright: string[];
21
+ text: string | null;
22
+ isPresent: boolean;
23
+ };
24
+
25
+ const MANIFEST_FILENAME = "package.json";
26
+ const GIT_PREFIX = /^git\+/;
27
+ const GIT_SUFFIX = /\.git$/;
28
+ const GIT_SCHEME = /^git:\/\//;
29
+ const GITHUB_SLUG = /^(?:github:)?(?<slug>[\w.-]+\/[\w.-]+)$/;
30
+
31
+ const realPath = (path: string): string => {
32
+ try {
33
+ return realpathSync(path);
34
+ } catch {
35
+ return path;
36
+ }
37
+ };
38
+
39
+ const optionalString = (value: unknown): string | null => (typeof value === "string" ? value : null);
40
+
41
+ const recordedPackage = (entry: unknown): RecordedPackage | null => {
42
+ if (!isRecord(entry) || typeof entry.name !== "string" || typeof entry.dir !== "string") {
43
+ return null;
44
+ }
45
+
46
+ return { name: entry.name, version: optionalString(entry.version), dir: entry.dir };
47
+ };
48
+
49
+ const recordedList = (value: unknown): RecordedPackage[] => {
50
+ if (!Array.isArray(value)) {
51
+ return [];
52
+ }
53
+
54
+ const entries: unknown[] = value;
55
+
56
+ return entries.map((entry) => recordedPackage(entry)).filter((entry) => entry !== null);
57
+ };
58
+
59
+ const readRecordedPackages = (settings: DeploySettings): RecordedPackage[] => {
60
+ const path = join(settings.paths.dist, BUNDLED_PACKAGES_FILENAME);
61
+
62
+ if (!existsSync(path)) {
63
+ throw new Error(`Cannot deploy: ${path} is missing. Run \`gtkx build\` first.`);
64
+ }
65
+
66
+ const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
67
+
68
+ return recordedList(isRecord(parsed) ? parsed.packages : null);
69
+ };
70
+
71
+ const authorLine = (manifest: PackageManifest): string[] => {
72
+ const { name, email } = manifest.author;
73
+
74
+ if (name === null) {
75
+ return [];
76
+ }
77
+
78
+ return [email === null ? name : `${name} <${email}>`];
79
+ };
80
+
81
+ const sourceUrl = (manifest: PackageManifest): string | null => {
82
+ const configured = manifest.repository ?? manifest.homepage;
83
+
84
+ if (configured === null) {
85
+ return null;
86
+ }
87
+
88
+ const url = configured.replace(GIT_PREFIX, "").replace(GIT_SUFFIX, "").replace(GIT_SCHEME, "https://");
89
+ const slug = GITHUB_SLUG.exec(url)?.groups?.slug;
90
+
91
+ if (slug !== undefined) {
92
+ return `https://github.com/${slug}`;
93
+ }
94
+
95
+ return url.startsWith("http") ? url : null;
96
+ };
97
+
98
+ const missingPackage = (entry: RecordedPackage): BundledPackage => ({
99
+ name: entry.name,
100
+ version: entry.version,
101
+ license: null,
102
+ source: null,
103
+ copyright: [],
104
+ text: null,
105
+ isPresent: false,
106
+ });
107
+
108
+ const packageIn = (entry: RecordedPackage, dir: string): BundledPackage => {
109
+ if (!existsSync(join(dir, MANIFEST_FILENAME))) {
110
+ return missingPackage(entry);
111
+ }
112
+
113
+ const manifest = readPackageManifest(dir);
114
+ const text = licenseTextIn(dir);
115
+ const copyright = copyrightLines(text);
116
+
117
+ return {
118
+ name: manifest.name ?? entry.name,
119
+ version: manifest.version ?? entry.version,
120
+ license: manifest.license,
121
+ source: sourceUrl(manifest),
122
+ copyright: copyright.length === 0 ? authorLine(manifest) : copyright,
123
+ text,
124
+ isPresent: true,
125
+ };
126
+ };
127
+
128
+ const keyFor = (entry: BundledPackage): string => `${entry.name}@${entry.version ?? ""}`;
129
+
130
+ const dedupe = (entries: BundledPackage[]): BundledPackage[] => {
131
+ const unique: Map<string, BundledPackage> = new Map();
132
+
133
+ for (const entry of entries) {
134
+ unique.set(keyFor(entry), entry);
135
+ }
136
+
137
+ return sortStringsBy(unique.values(), keyFor);
138
+ };
139
+
140
+ const warnMissing = (entries: BundledPackage[]): void => {
141
+ const missing = entries.filter((entry) => !entry.isPresent);
142
+
143
+ if (missing.length === 0) {
144
+ return;
145
+ }
146
+
147
+ warn(
148
+ `The third-party notices name ${missing.map((entry) => keyFor(entry)).join(", ")} without any license: ` +
149
+ "`gtkx build` recorded where each package was installed and those directories are no longer there. " +
150
+ "Deploy from the tree the build ran in, or build again, so the notices carry their terms.",
151
+ );
152
+ };
153
+
154
+ const bundledPackages = (settings: DeploySettings): BundledPackage[] => {
155
+ const root = realPath(settings.paths.root);
156
+
157
+ const entries = readRecordedPackages(settings)
158
+ .map((entry) => ({ entry, dir: resolve(settings.paths.dist, entry.dir) }))
159
+ .filter(({ dir }) => realPath(dir) !== root)
160
+ .map(({ entry, dir }) => packageIn(entry, dir));
161
+
162
+ const bundled = dedupe(entries);
163
+ warnMissing(bundled);
164
+
165
+ return bundled;
166
+ };
167
+
168
+ export { type BundledPackage, bundledPackages };
@@ -0,0 +1,52 @@
1
+ import type { DeploySettings, Notice, NoticeSection } from "../types.js";
2
+
3
+ const RULE_WIDTH = 100;
4
+ const HEADING = "THIRD-PARTY NOTICES";
5
+ const SECTION_RULE = "=".repeat(RULE_WIDTH);
6
+ const NOTICE_RULE = "-".repeat(RULE_WIDTH);
7
+ const FILE_SEPARATOR = ", ";
8
+
9
+ const introFor = (settings: DeploySettings): string[] => [
10
+ HEADING,
11
+ `${settings.name} ${settings.versions.upstream}`,
12
+ "",
13
+ `This file lists the software that ships inside ${settings.name} without having been written by its`,
14
+ "authors, and the terms each piece of it is distributed under.",
15
+ ];
16
+
17
+ const headerFor = (section: NoticeSection): string[] => [
18
+ SECTION_RULE,
19
+ section.title,
20
+ ...(section.files.length === 0 ? [] : [`Files: ${section.files.join(FILE_SEPARATOR)}`]),
21
+ SECTION_RULE,
22
+ "",
23
+ ];
24
+
25
+ const detailLines = (notice: Notice): string[] => [
26
+ `License: ${notice.license}`,
27
+ ...(notice.source === null ? [] : [`Source: ${notice.source}`]),
28
+ ];
29
+
30
+ const bodyLines = (notice: Notice): string[] =>
31
+ notice.text === null ? notice.copyright : ["", notice.text];
32
+
33
+ const renderNotice = (notice: Notice): string[] => [
34
+ NOTICE_RULE,
35
+ notice.subject,
36
+ ...detailLines(notice),
37
+ NOTICE_RULE,
38
+ ...bodyLines(notice),
39
+ "",
40
+ ];
41
+
42
+ const renderSection = (section: NoticeSection): string[] => [
43
+ ...headerFor(section),
44
+ ...section.summary,
45
+ "",
46
+ ...section.notices.flatMap((notice) => renderNotice(notice)),
47
+ ];
48
+
49
+ const renderNotices = (settings: DeploySettings, sections: NoticeSection[]): string =>
50
+ [...introFor(settings), "", ...sections.flatMap((section) => renderSection(section)), ""].join("\n");
51
+
52
+ export { renderNotices };
@@ -0,0 +1,42 @@
1
+ import { readdirSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ const LICENSE_FILE = /^(?:licen[cs]e|copying|notice)(?:[-._].*)?$/i;
5
+ const COPYRIGHT_LINE = /^(?:Copyright\b|©)/;
6
+ const MAX_COPYRIGHT_LINES = 20;
7
+ const MORE_COPYRIGHT = "and the other holders named in the license text";
8
+
9
+ const readLicenseText = (path: string): string | null => {
10
+ try {
11
+ return readFileSync(path, "utf8").trimEnd();
12
+ } catch {
13
+ return null;
14
+ }
15
+ };
16
+
17
+ const licenseFilesIn = (dir: string): string[] =>
18
+ readdirSync(dir, { withFileTypes: true })
19
+ .filter((entry) => entry.isFile() && LICENSE_FILE.test(entry.name))
20
+ .map((entry) => entry.name)
21
+ .toSorted((left, right) => left.localeCompare(right));
22
+
23
+ const licenseTextIn = (dir: string): string | null => {
24
+ const texts = licenseFilesIn(dir)
25
+ .map((name) => readLicenseText(join(dir, name)))
26
+ .filter((text) => text !== null);
27
+
28
+ return texts.length === 0 ? null : texts.join("\n\n");
29
+ };
30
+
31
+ const copyrightLines = (text: string | null): string[] => {
32
+ const found = (text ?? "")
33
+ .split("\n")
34
+ .map((line) => line.trim())
35
+ .filter((line) => COPYRIGHT_LINE.test(line));
36
+
37
+ const unique = [...new Set(found)];
38
+
39
+ return unique.length > MAX_COPYRIGHT_LINES ? [...unique.slice(0, MAX_COPYRIGHT_LINES), MORE_COPYRIGHT] : unique;
40
+ };
41
+
42
+ export { copyrightLines, licenseTextIn, readLicenseText };
@@ -7,6 +7,7 @@ const EXECUTABLE_MODE = 0o755;
7
7
  const READABLE_MODE = 0o644;
8
8
  const EXECUTABLE_SUFFIXES = [".node", ".so"];
9
9
  const MODE_MASK = 0o7777;
10
+ const EXECUTE_MASK = 0o111;
10
11
 
11
12
  const modeFor = (rel: string): number => {
12
13
  const name = rel.split("/").at(-1) ?? rel;
@@ -44,8 +45,11 @@ const writeInto = (root: string, rel: string, contents: string, mode = READABLE_
44
45
 
45
46
  const sourceMode = (path: string): number => statSync(path).mode & MODE_MASK;
46
47
 
48
+ const executableModeFor = (path: string): number =>
49
+ (sourceMode(path) & EXECUTE_MASK) === 0 ? READABLE_MODE : EXECUTABLE_MODE;
50
+
47
51
  const copyTree = (root: string, relBase: string, sourceDir: string): StagedFile[] =>
48
52
  listFilesRecursive(sourceDir)
49
53
  .map((file) => copyInto(root, join(relBase, file.rel), file.absPath, sourceMode(file.absPath)));
50
54
 
51
- export { copyInto, copyTree, EXECUTABLE_MODE, writeInto };
55
+ export { copyInto, copyTree, EXECUTABLE_MODE, executableModeFor, writeInto };
@@ -1,13 +1,15 @@
1
1
  import { sortStringsBy } from "@gtkx/utils";
2
2
  import { existsSync, rmSync } from "node:fs";
3
3
  import { join, resolve } from "node:path";
4
- import type { DeploySettings, DeployTargetName, NodeRuntime, StagedFile } from "../types.js";
4
+ import type { DeploySettings, DeployTargetName, NodeRuntime, NoticeSection, StagedFile } from "../types.js";
5
5
  import { listFilesRecursive } from "../../internal/list-files.js";
6
+ import { BUNDLED_PACKAGES_FILENAME } from "../../vite-plugins/bundled-packages.js";
6
7
  import { BUNDLE_FILENAME } from "../../vite-plugins/esm-extension.js";
7
8
  import { renderCopyright } from "../freedesktop/copyright.js";
8
9
  import { renderDbusService } from "../freedesktop/dbus-service.js";
9
10
  import { renderDesktopEntry } from "../freedesktop/desktop-entry.js";
10
- import { copyInto, EXECUTABLE_MODE, writeInto } from "./copy-tree.js";
11
+ import { renderNotices } from "../notices/render.js";
12
+ import { copyInto, EXECUTABLE_MODE, executableModeFor, writeInto } from "./copy-tree.js";
11
13
  import { stageIcons } from "./icons.js";
12
14
  import { NODE_FILENAME, renderLauncher } from "./launcher.js";
13
15
  import { stageSchemas } from "./schemas.js";
@@ -25,6 +27,8 @@ type StagedMetadata = {
25
27
  };
26
28
 
27
29
  const DIST_ICONS_DIR = "icons";
30
+ const NOTICES_FILENAME = "THIRD-PARTY-NOTICES";
31
+ const NODE_LICENSE_FILENAME = "LICENSE";
28
32
  const SHARE_APPLICATIONS = "share/applications";
29
33
  const SHARE_METAINFO = "share/metainfo";
30
34
  const SHARE_MIME_PACKAGES = "share/mime/packages";
@@ -40,7 +44,14 @@ const PREFIX_FOR: Record<DeployTargetName, string> = {
40
44
  };
41
45
 
42
46
  const libDirFor = (settings: DeploySettings): string => `lib/${settings.binaryName}`;
47
+ const licenseDirFor = (settings: DeploySettings): string => `${SHARE_LICENSES}/${settings.binaryName}`;
48
+ const licenseDestination = (settings: DeploySettings): string => `${licenseDirFor(settings)}/LICENSE`;
49
+ const noticesDestination = (settings: DeploySettings): string => `${licenseDirFor(settings)}/${NOTICES_FILENAME}`;
43
50
  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 nodeLicenseDestination = (settings: DeploySettings): string =>
54
+ `${licenseDirFor(settings)}/${NODE_FILENAME}/${NODE_LICENSE_FILENAME}`;
44
55
 
45
56
  const stageRuntimeFiles = (settings: DeploySettings, root: string): StagedFile[] => {
46
57
  const dist = settings.paths.dist;
@@ -50,7 +61,7 @@ const stageRuntimeFiles = (settings: DeploySettings, root: string): StagedFile[]
50
61
  }
51
62
 
52
63
  return listFilesRecursive(dist)
53
- .filter((file) => !isIconAsset(file.rel))
64
+ .filter((file) => !isIconAsset(file.rel) && !isBuildMetadata(file.rel))
54
65
  .map((file) => copyInto(root, join(libDirFor(settings), file.rel), file.absPath));
55
66
  };
56
67
 
@@ -66,8 +77,11 @@ const stageMetadata = (settings: DeploySettings, root: string, metadata: StagedM
66
77
  ];
67
78
 
68
79
  const stageExtraFiles = (settings: DeploySettings, root: string): StagedFile[] =>
69
- Object.entries(settings.extraFiles).map(([destination, source]) =>
70
- copyInto(root, destination, resolve(settings.paths.root, source)));
80
+ settings.extraFiles.map((file) => {
81
+ const source = resolve(settings.paths.root, file.source);
82
+
83
+ return copyInto(root, file.destination, source, file.mode ?? executableModeFor(source));
84
+ });
71
85
 
72
86
  const byRelativePath = (files: StagedFile[]): StagedFile[] => {
73
87
  const latest: Map<string, StagedFile> = new Map();
@@ -114,7 +128,7 @@ const stageActivation = (settings: DeploySettings, root: string, target: DeployT
114
128
  )];
115
129
  };
116
130
 
117
- const stageOverlay = (settings: DeploySettings, target: DeployTargetName): StagedFile[] => {
131
+ const stageOverlay = (settings: DeploySettings, target: DeployTargetName, notices: NoticeSection[]): StagedFile[] => {
118
132
  const root = join(settings.paths.overlay, target);
119
133
  rmSync(root, { recursive: true, force: true, maxRetries: 5 });
120
134
  const licenseFile = settings.paths.licenseFile;
@@ -123,23 +137,33 @@ const stageOverlay = (settings: DeploySettings, target: DeployTargetName): Stage
123
137
  if (target === "deb") {
124
138
  return [
125
139
  ...activation,
126
- writeInto(root, join(SHARE_DOC, settings.binaryName, "copyright"), renderCopyright(settings)),
140
+ writeInto(root, join(SHARE_DOC, settings.binaryName, "copyright"), renderCopyright(settings, notices)),
127
141
  ];
128
142
  }
129
143
 
130
144
  return [
131
145
  ...activation,
132
- ...(licenseFile === null
133
- ? []
134
- : [copyInto(root, join(SHARE_LICENSES, settings.binaryName, "LICENSE"), licenseFile)]),
146
+ ...(licenseFile === null ? [] : [copyInto(root, licenseDestination(settings), licenseFile)]),
147
+ writeInto(root, noticesDestination(settings), renderNotices(settings, notices)),
135
148
  ];
136
149
  };
137
150
 
138
- const stageOverlays = (settings: DeploySettings): Record<DeployTargetName, StagedFile[]> => ({
139
- appimage: stageOverlay(settings, "appimage"),
140
- deb: stageOverlay(settings, "deb"),
141
- flatpak: stageOverlay(settings, "flatpak"),
142
- rpm: stageOverlay(settings, "rpm"),
151
+ const stageOverlays = (
152
+ settings: DeploySettings,
153
+ notices: NoticeSection[],
154
+ ): Record<DeployTargetName, StagedFile[]> => ({
155
+ appimage: stageOverlay(settings, "appimage", notices),
156
+ deb: stageOverlay(settings, "deb", notices),
157
+ flatpak: stageOverlay(settings, "flatpak", notices),
158
+ rpm: stageOverlay(settings, "rpm", notices),
143
159
  });
144
160
 
145
- export { type StagedMetadata, stageOverlays, stagePayload };
161
+ export {
162
+ licenseDestination,
163
+ nodeLicenseDestination,
164
+ NOTICES_FILENAME,
165
+ noticesDestination,
166
+ type StagedMetadata,
167
+ stageOverlays,
168
+ stagePayload,
169
+ };
@@ -17,11 +17,13 @@ import { renderMetainfo } from "./freedesktop/metainfo.js";
17
17
  import { renderMimePackage } from "./freedesktop/mime-package.js";
18
18
  import { validateDesktopEntry, validateMetainfo } from "./freedesktop/validate.js";
19
19
  import { resolveNodeRuntime } from "./node-runtime/index.js";
20
+ import { collectNotices } from "./notices/collect.js";
20
21
  import { type StagedMetadata, stageOverlays, stagePayload } from "./payload/stage.js";
21
22
  import { DEFAULT_TARGETS, parseTargetList, targetsFor } from "./registry.js";
22
23
  import { resolveDeploySettings } from "./settings/index.js";
23
24
  import { readPackageManifest } from "./settings/package-manifest.js";
24
25
  import { missingDeployError } from "./settings/starter.js";
26
+ import { finishArgsFor, hasDisplaySocket } from "./targets/flatpak-manifest.js";
25
27
  import { detectPackageManager } from "./targets/flatpak-sources.js";
26
28
  import {
27
29
  APPSTREAMCLI,
@@ -78,10 +80,19 @@ const validateMetadata = (settings: DeploySettings, metadata: StagedMetadata, ar
78
80
  info("Validated the desktop entry and the metainfo");
79
81
  };
80
82
 
81
- const warnMissingNetwork = (settings: DeploySettings): void => {
82
- const finishArgs = settings.deploy.flatpak?.finishArgs;
83
+ const warnMissingDisplay = (settings: DeploySettings, finishArgs: string[]): void => {
84
+ if (hasDisplaySocket(finishArgs)) {
85
+ return;
86
+ }
83
87
 
84
- if (finishArgs === undefined || finishArgs.includes(NETWORK_ARG)) {
88
+ warn(
89
+ `The flatpak permissions grant no display socket, so ${settings.name} will start without a window. ` +
90
+ "Grant one through `deploy.flatpak.finishArgs`, such as `--socket=wayland`.",
91
+ );
92
+ };
93
+
94
+ const warnMissingNetwork = (settings: DeploySettings, finishArgs: string[]): void => {
95
+ if (settings.deploy.flatpak?.finishArgs === undefined || finishArgs.includes(NETWORK_ARG)) {
85
96
  return;
86
97
  }
87
98
 
@@ -92,6 +103,16 @@ const warnMissingNetwork = (settings: DeploySettings): void => {
92
103
  warn(`This app declares ${WEBKIT_LIBRARY} but its flatpak permissions omit ${NETWORK_ARG}, so pages will not load`);
93
104
  };
94
105
 
106
+ const warnFlatpakPermissions = (targets: DeployTarget[], settings: DeploySettings): void => {
107
+ if (targets.every((target) => target.name !== "flatpak")) {
108
+ return;
109
+ }
110
+
111
+ const finishArgs = finishArgsFor(settings);
112
+ warnMissingDisplay(settings, finishArgs);
113
+ warnMissingNetwork(settings, finishArgs);
114
+ };
115
+
95
116
  const sourceModeTools = (targets: DeployTarget[], settings: DeploySettings): DeployTool[] => {
96
117
  const isFlatpakSource = settings.deploy.flatpak?.mode === "source";
97
118
 
@@ -125,7 +146,7 @@ const preflight = (targets: DeployTarget[], settings: DeploySettings, shouldPrin
125
146
  const report = probeTools([...required, ...packagerTools]);
126
147
  assertTools(report);
127
148
  warnMissingOptional(report);
128
- warnMissingNetwork(settings);
149
+ warnFlatpakPermissions(targets, settings);
129
150
  };
130
151
 
131
152
  const resolveTargetNames = (options: DeployOptions, settings: DeploySettings): string[] => {
@@ -165,8 +186,9 @@ const buildPayload = async (
165
186
 
166
187
  const stage = stagePayload({ settings, node, metadata });
167
188
  info(`Staged ${String(stage.length)} files into ${displayPath(settings, settings.paths.stage)}`);
189
+ const notices = collectNotices({ settings, node });
168
190
 
169
- return { settings, node, stage, overlays: stageOverlays(settings) };
191
+ return { settings, node, stage, notices, overlays: stageOverlays(settings, notices) };
170
192
  };
171
193
 
172
194
  const renderTargetManifests = (
@@ -0,0 +1,21 @@
1
+ import type { DeployConfig, DeployExtraFile } from "../types.js";
2
+
3
+ type ExtraFiles = NonNullable<DeployConfig["extraFiles"]>;
4
+ type ExtraFileEntry = NonNullable<ExtraFiles[string]>;
5
+
6
+ const OCTAL_RADIX = 8;
7
+
8
+ const resolveExtraFile = (destination: string, entry: ExtraFileEntry): DeployExtraFile => {
9
+ if (typeof entry === "string") {
10
+ return { destination, source: entry, mode: null };
11
+ }
12
+
13
+ const mode = entry.mode === undefined ? null : Number.parseInt(entry.mode, OCTAL_RADIX);
14
+
15
+ return { destination, source: entry.source, mode };
16
+ };
17
+
18
+ const resolveExtraFiles = (deploy: DeployConfig): DeployExtraFile[] =>
19
+ Object.entries(deploy.extraFiles ?? {}).map(([destination, entry]) => resolveExtraFile(destination, entry));
20
+
21
+ export { resolveExtraFiles };
@@ -9,6 +9,7 @@ import type {
9
9
  } from "../types.js";
10
10
  import { resolveArch } from "./arch.js";
11
11
  import { resolveExecToken, resolveFileAssociations, resolveMimeTypes } from "./exec.js";
12
+ import { resolveExtraFiles } from "./extra-files.js";
12
13
  import {
13
14
  type IdentityRequest,
14
15
  resolveBinaryName,
@@ -177,7 +178,7 @@ const resolveDeploySettings = (request: SettingsRequest): DeploySettings => {
177
178
  ...identitySlice(core),
178
179
  ...metadataSlice(request.root, core),
179
180
  ...desktopSlice(deploy),
180
- extraFiles: deploy.extraFiles ?? {},
181
+ extraFiles: resolveExtraFiles(deploy),
181
182
  versions: core.versions,
182
183
  arch: resolveArch(),
183
184
  paths: resolvePaths({ root: request.root, deploy, outDirOverride: request.outDirOverride }),
@@ -13,6 +13,7 @@ type PackageManifest = {
13
13
  description: string | null;
14
14
  license: string | null;
15
15
  homepage: string | null;
16
+ repository: string | null;
16
17
  packageManager: string | null;
17
18
  author: ManifestAuthor;
18
19
  };
@@ -29,6 +30,14 @@ const parseAuthorString = (author: string): ManifestAuthor => {
29
30
  return { name: optionalString(groups?.name), email: optionalString(groups?.email) };
30
31
  };
31
32
 
33
+ const parseRepository = (repository: unknown): string | null => {
34
+ if (isRecord(repository)) {
35
+ return optionalString(repository.url);
36
+ }
37
+
38
+ return optionalString(repository);
39
+ };
40
+
32
41
  const parseAuthor = (author: unknown): ManifestAuthor => {
33
42
  if (typeof author === "string") {
34
43
  return parseAuthorString(author);
@@ -60,6 +69,7 @@ const readPackageManifest = (root: string): PackageManifest => {
60
69
  description: optionalString(manifest.description),
61
70
  license: optionalString(manifest.license),
62
71
  homepage: optionalString(manifest.homepage),
72
+ repository: parseRepository(manifest.repository),
63
73
  packageManager: optionalString(manifest.packageManager),
64
74
  author: parseAuthor(manifest.author),
65
75
  };
@@ -112,4 +112,4 @@ const resolvePaths = (request: PathsRequest): DeployPaths => {
112
112
  };
113
113
  };
114
114
 
115
- export { resolvePaths };
115
+ export { isInside, resolvePaths };