@synopackageland/cli 0.7.0 → 0.7.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.
- package/CHANGELOG.md +301 -1
- package/assets/default-icon/README.md +19 -0
- package/assets/default-icon/default-icon-small.svg +9 -0
- package/assets/default-icon/default-icon.svg +10 -0
- package/assets/default-icon/icon_16.png +0 -0
- package/assets/default-icon/icon_24.png +0 -0
- package/assets/default-icon/icon_256.png +0 -0
- package/assets/default-icon/icon_32.png +0 -0
- package/assets/default-icon/icon_48.png +0 -0
- package/assets/default-icon/icon_64.png +0 -0
- package/assets/default-icon/icon_72.png +0 -0
- package/dist/add.d.ts +4 -1
- package/dist/add.js +14 -9
- package/dist/build-spk/dsm-icons.d.ts +31 -2
- package/dist/build-spk/dsm-icons.js +135 -56
- package/dist/build-spk/dsm-platforms.json +4 -1
- package/dist/build-spk/gateway.d.ts +1 -1
- package/dist/build-spk/gateway.js +8 -4
- package/dist/build-spk/generator.js +17 -3
- package/dist/build-spk/go-toolchain.d.ts +20 -0
- package/dist/build-spk/go-toolchain.js +44 -0
- package/dist/build-spk/png.d.ts +14 -0
- package/dist/build-spk/png.js +161 -0
- package/dist/build-spk/ui-texts.d.ts +20 -0
- package/dist/build-spk/ui-texts.js +42 -0
- package/dist/cli.js +39 -9
- package/dist/contract/i18n.d.ts +5 -0
- package/dist/contract/i18n.js +45 -0
- package/dist/contract/icon.d.ts +5 -0
- package/dist/contract/icon.js +27 -0
- package/dist/contract/validate.js +4 -0
- package/dist/contract/web-launch.js +6 -2
- package/dist/deploy/native-install-values.d.ts +17 -0
- package/dist/deploy/native-install-values.js +32 -0
- package/dist/deploy/plan.d.ts +6 -2
- package/dist/deploy/plan.js +57 -6
- package/dist/error-registry.js +15 -6
- package/dist/host/dsm-client.js +53 -17
- package/dist/host/dsm-http.js +24 -2
- package/dist/host/types.d.ts +1 -0
- package/dist/info.js +28 -9
- package/dist/init.js +1 -1
- package/dist/runtime/dsm-version.d.ts +6 -0
- package/dist/runtime/dsm-version.js +8 -0
- package/dist/runtime/executor-build.d.ts +1 -1
- package/dist/runtime/executor-build.js +8 -5
- package/dist/standalone/daemon.d.ts +2 -1
- package/dist/standalone/daemon.js +10 -6
- package/dist/standalone/dsm-window.d.ts +2 -0
- package/dist/standalone/dsm-window.js +26 -40
- package/dist/standalone/i18n.d.ts +12 -0
- package/dist/standalone/i18n.js +30 -0
- package/dist/standalone/inspect-spk.d.ts +5 -0
- package/dist/standalone/inspect-spk.js +41 -2
- package/dist/standalone/manifest.d.ts +5 -6
- package/dist/standalone/manifest.js +21 -34
- package/dist/standalone/native-authorization-window.d.ts +1 -1
- package/dist/standalone/native-authorization-window.js +7 -1
- package/dist/standalone/native-layout.d.ts +2 -0
- package/dist/standalone/native-layout.js +22 -1
- package/dist/standalone/native-root-window.d.ts +4 -2
- package/dist/standalone/native-root-window.js +33 -18
- package/dist/standalone/root-window.d.ts +4 -1
- package/dist/standalone/root-window.js +143 -33
- package/dist/standalone/spk.js +7 -4
- package/dist/templates/_daemon/go.mod +1 -1
- package/dist/templates/_daemon/main.go +6 -3
- package/dist/templates/_daemon/main_test.go +21 -1
- package/dist/templates/_daemon/testdata/child.go +3 -0
- package/dist/templates/_executor/README.md +4 -1
- package/dist/templates/_executor/anchor.go +10 -0
- package/dist/templates/_executor/anchor_test.go +40 -5
- package/dist/templates/_executor/digest_review_test.go +2 -2
- package/dist/templates/_executor/dsm.go +40 -3
- package/dist/templates/_executor/go.mod +2 -0
- package/dist/templates/_executor/main.go +11 -5
- package/dist/templates/_executor/managed.go +3 -3
- package/dist/templates/_executor/managed_test.go +3 -3
- package/dist/templates/_executor/path_test.go +18 -0
- package/dist/templates/_executor/pid.go +37 -0
- package/dist/templates/_executor/pid_test.go +113 -0
- package/dist/templates/_executor/platform_other.go +8 -0
- package/dist/templates/_executor/purge_log.go +18 -0
- package/dist/templates/_executor/tasks.go +45 -7
- package/dist/templates/_executor/tasks_dsm71_test.go +114 -0
- package/dist/templates/_executor/tasks_test.go +58 -1
- package/dist/templates/_executor/upgrade_watch_test.go +13 -5
- package/dist/templates/_gateway/go.mod +2 -0
- package/dist/templates/binary/dsm-package.yaml +3 -0
- package/dist/templates/compose/synology-app.yaml +3 -0
- package/dist/templates/native-go/backend/go.mod +1 -1
- package/dist/templates/native-go/backend/internal/filestation/client.go +39 -3
- package/dist/templates/native-go/backend/internal/filestation/download_dsm71_test.go +82 -0
- package/dist/templates/native-go/backend/internal/identity/README.md +5 -1
- package/dist/templates/native-go/backend/internal/identity/dsm.go +24 -5
- package/dist/templates/native-go/backend/internal/identity/http.go +1 -1
- package/dist/templates/native-go/backend/internal/identity/identity_test.go +48 -1
- package/dist/templates/native-go/backend/internal/identity/service.go +1 -1
- package/dist/templates/native-go/dsm-package.yaml +3 -0
- package/dist/templates/native-go-delegate/frontend/test/delegate.test.ts +15 -0
- package/dist/templates/native-go-root-ops/backend/internal/app/executor.go +30 -2
- package/dist/templates/native-go-root-ops/backend/internal/app/executor_test.go +64 -0
- package/dist/templates/native-go-vue/frontend/src/delegate/ExecutorSettings.vue +3 -2
- package/dist/templates/native-go-vue/frontend/src/delegate/executor-settings.test.ts +23 -0
- package/dist/templates/native-go-vue/frontend/src/delegate/executor-setup.ts +5 -1
- package/dist/templates/native-go-vue/frontend/src/delegate/i18n/de.json +1 -0
- package/dist/templates/native-go-vue/frontend/src/delegate/i18n/en.json +1 -0
- package/dist/templates/native-go-vue/frontend/src/delegate/i18n/es.json +1 -0
- package/dist/templates/native-go-vue/frontend/src/delegate/i18n/fr.json +1 -0
- package/dist/templates/native-go-vue/frontend/src/delegate/i18n/it.json +1 -0
- package/dist/templates/native-go-vue/frontend/src/delegate/i18n/ja.json +1 -0
- package/dist/templates/native-go-vue/frontend/src/delegate/i18n/ko.json +1 -0
- package/dist/templates/native-go-vue/frontend/src/delegate/i18n/zh-CN.json +1 -0
- package/dist/templates/native-go-vue/frontend/src/delegate/i18n/zh-TW.json +1 -0
- package/package.json +2 -1
- package/schema/synology-app.schema.json +22 -0
- package/templates/_daemon/go.mod +1 -1
- package/templates/_daemon/main.go +6 -3
- package/templates/_daemon/main_test.go +21 -1
- package/templates/_daemon/testdata/child.go +3 -0
- package/templates/_executor/README.md +4 -1
- package/templates/_executor/anchor.go +10 -0
- package/templates/_executor/anchor_test.go +40 -5
- package/templates/_executor/digest_review_test.go +2 -2
- package/templates/_executor/dsm.go +40 -3
- package/templates/_executor/go.mod +2 -0
- package/templates/_executor/main.go +11 -5
- package/templates/_executor/managed.go +3 -3
- package/templates/_executor/managed_test.go +3 -3
- package/templates/_executor/path_test.go +18 -0
- package/templates/_executor/pid.go +37 -0
- package/templates/_executor/pid_test.go +113 -0
- package/templates/_executor/platform_other.go +8 -0
- package/templates/_executor/purge_log.go +18 -0
- package/templates/_executor/tasks.go +45 -7
- package/templates/_executor/tasks_dsm71_test.go +114 -0
- package/templates/_executor/tasks_test.go +58 -1
- package/templates/_executor/upgrade_watch_test.go +13 -5
- package/templates/_gateway/go.mod +2 -0
- package/templates/binary/dsm-package.yaml +3 -0
- package/templates/compose/synology-app.yaml +3 -0
- package/templates/native-go/backend/go.mod +1 -1
- package/templates/native-go/backend/internal/filestation/client.go +39 -3
- package/templates/native-go/backend/internal/filestation/download_dsm71_test.go +82 -0
- package/templates/native-go/backend/internal/identity/README.md +5 -1
- package/templates/native-go/backend/internal/identity/dsm.go +24 -5
- package/templates/native-go/backend/internal/identity/http.go +1 -1
- package/templates/native-go/backend/internal/identity/identity_test.go +48 -1
- package/templates/native-go/backend/internal/identity/service.go +1 -1
- package/templates/native-go/dsm-package.yaml +3 -0
- package/templates/native-go-delegate/frontend/test/delegate.test.ts +15 -0
- package/templates/native-go-root-ops/backend/internal/app/executor.go +30 -2
- package/templates/native-go-root-ops/backend/internal/app/executor_test.go +64 -0
- package/templates/native-go-vue/frontend/src/delegate/ExecutorSettings.vue +3 -2
- package/templates/native-go-vue/frontend/src/delegate/executor-settings.test.ts +23 -0
- package/templates/native-go-vue/frontend/src/delegate/executor-setup.ts +5 -1
- package/templates/native-go-vue/frontend/src/delegate/i18n/de.json +1 -0
- package/templates/native-go-vue/frontend/src/delegate/i18n/en.json +1 -0
- package/templates/native-go-vue/frontend/src/delegate/i18n/es.json +1 -0
- package/templates/native-go-vue/frontend/src/delegate/i18n/fr.json +1 -0
- package/templates/native-go-vue/frontend/src/delegate/i18n/it.json +1 -0
- package/templates/native-go-vue/frontend/src/delegate/i18n/ja.json +1 -0
- package/templates/native-go-vue/frontend/src/delegate/i18n/ko.json +1 -0
- package/templates/native-go-vue/frontend/src/delegate/i18n/zh-CN.json +1 -0
- package/templates/native-go-vue/frontend/src/delegate/i18n/zh-TW.json +1 -0
|
@@ -1,67 +1,146 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join, relative } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { decodePng, encodePng } from "./png.js";
|
|
5
|
+
/** Sizes DSM loads for `images/app_{0}.png` (main menu, desktop, window, taskbar). */
|
|
6
|
+
export const WINDOW_ICON_SIZES = [16, 24, 32, 48, 64, 72, 256];
|
|
7
|
+
/** The package source drops a release whose PACKAGE_ICON is larger than this. */
|
|
8
|
+
export const MAX_ICON_BYTES = 256 * 1024;
|
|
9
|
+
const ICON_FILE = /^icon_(\d+)\.png$/;
|
|
10
|
+
/** Ships next to dist/ and src/ alike: <package>/assets/default-icon. */
|
|
11
|
+
export const DEFAULT_ICON_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "assets", "default-icon");
|
|
12
|
+
function readPng(path, name) {
|
|
13
|
+
try {
|
|
14
|
+
return decodePng(readFileSync(path));
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
throw new Error(`${name}:${error.message}`);
|
|
13
18
|
}
|
|
14
|
-
return (crc ^ 0xffffffff) >>> 0;
|
|
15
|
-
}
|
|
16
|
-
function pngChunk(type, data) {
|
|
17
|
-
const length = Buffer.alloc(4);
|
|
18
|
-
length.writeUInt32BE(data.length);
|
|
19
|
-
const typeBuf = Buffer.from(type, "ascii");
|
|
20
|
-
const crc = Buffer.alloc(4);
|
|
21
|
-
crc.writeUInt32BE(crc32(Buffer.concat([typeBuf, data])));
|
|
22
|
-
return Buffer.concat([length, typeBuf, data, crc]);
|
|
23
19
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
/** Area-average downscale in premultiplied alpha, so transparent edges stay clean. */
|
|
21
|
+
function scale(image, size) {
|
|
22
|
+
if (image.width === size && image.height === size)
|
|
23
|
+
return image;
|
|
24
|
+
const { width, height, rgba } = image;
|
|
25
|
+
const premultiplied = new Float64Array(width * height * 4);
|
|
26
|
+
for (let i = 0; i < width * height; i++) {
|
|
27
|
+
const alpha = rgba[i * 4 + 3] / 255;
|
|
28
|
+
for (let c = 0; c < 3; c++)
|
|
29
|
+
premultiplied[i * 4 + c] = rgba[i * 4 + c] * alpha;
|
|
30
|
+
premultiplied[i * 4 + 3] = rgba[i * 4 + 3];
|
|
31
|
+
}
|
|
32
|
+
const spans = (from) => Array.from({ length: size }, (_, d) => {
|
|
33
|
+
const start = (d * from) / size, end = ((d + 1) * from) / size;
|
|
34
|
+
const weights = [];
|
|
35
|
+
for (let s = Math.floor(start); s < end; s++)
|
|
36
|
+
weights.push([s, Math.min(end, s + 1) - Math.max(start, s)]);
|
|
37
|
+
return weights.map(([s, w]) => [s, w / (end - start)]);
|
|
38
|
+
});
|
|
39
|
+
const columns = spans(width), rows = spans(height);
|
|
40
|
+
const across = new Float64Array(size * height * 4);
|
|
41
|
+
for (let y = 0; y < height; y++) {
|
|
42
|
+
for (let x = 0; x < size; x++) {
|
|
43
|
+
for (const [s, w] of columns[x]) {
|
|
44
|
+
for (let c = 0; c < 4; c++)
|
|
45
|
+
across[(y * size + x) * 4 + c] += premultiplied[(y * width + s) * 4 + c] * w;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const out = Buffer.alloc(size * size * 4);
|
|
27
50
|
for (let y = 0; y < size; y++) {
|
|
28
|
-
const row = y * stride;
|
|
29
|
-
raw[row] = 0;
|
|
30
51
|
for (let x = 0; x < size; x++) {
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
52
|
+
const pixel = [0, 0, 0, 0];
|
|
53
|
+
for (const [s, w] of rows[y])
|
|
54
|
+
for (let c = 0; c < 4; c++)
|
|
55
|
+
pixel[c] += across[(s * size + x) * 4 + c] * w;
|
|
56
|
+
const alpha = Math.round(pixel[3]);
|
|
57
|
+
const at = (y * size + x) * 4;
|
|
58
|
+
for (let c = 0; c < 3; c++)
|
|
59
|
+
out[at + c] = alpha ? Math.min(255, Math.round((pixel[c] * 255) / pixel[3])) : 0;
|
|
60
|
+
out[at + 3] = alpha;
|
|
35
61
|
}
|
|
36
62
|
}
|
|
37
|
-
|
|
38
|
-
ihdr.writeUInt32BE(size, 0);
|
|
39
|
-
ihdr.writeUInt32BE(size, 4);
|
|
40
|
-
ihdr[8] = 8;
|
|
41
|
-
ihdr[9] = 2;
|
|
42
|
-
return Buffer.concat([
|
|
43
|
-
Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]),
|
|
44
|
-
pngChunk("IHDR", ihdr),
|
|
45
|
-
pngChunk("IDAT", deflateSync(raw, { level: 9 })),
|
|
46
|
-
pngChunk("IEND", Buffer.alloc(0)),
|
|
47
|
-
]);
|
|
63
|
+
return { width: size, height: size, rgba: out };
|
|
48
64
|
}
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
65
|
+
function isSolid(image) {
|
|
66
|
+
const first = image.rgba.readUInt32BE(0);
|
|
67
|
+
for (let i = 4; i < image.rgba.length; i += 4)
|
|
68
|
+
if (image.rgba.readUInt32BE(i) !== first)
|
|
69
|
+
return false;
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
/** `icon_<size>.png` drawn per size; icon_256.png is required and scaled for the rest. */
|
|
73
|
+
function readIconDirectory(dir) {
|
|
74
|
+
const drawn = new Map();
|
|
75
|
+
for (const name of readdirSync(dir).sort()) {
|
|
76
|
+
const match = ICON_FILE.exec(name);
|
|
77
|
+
if (!match)
|
|
78
|
+
continue;
|
|
79
|
+
const size = Number(match[1]);
|
|
80
|
+
if (!WINDOW_ICON_SIZES.includes(size)) {
|
|
81
|
+
throw new Error(`${name}:不支援的尺寸;只接受 ${WINDOW_ICON_SIZES.map((s) => `icon_${s}.png`).join("、")}。`);
|
|
82
|
+
}
|
|
83
|
+
const image = readPng(join(dir, name), name);
|
|
84
|
+
if (image.width !== size || image.height !== size) {
|
|
85
|
+
throw new Error(`${name}:尺寸是 ${image.width}×${image.height},應為 ${size}×${size}。`);
|
|
86
|
+
}
|
|
87
|
+
drawn.set(size, image);
|
|
56
88
|
}
|
|
89
|
+
if (!drawn.has(256))
|
|
90
|
+
throw new Error(`圖示目錄 ${basename(dir)}/ 缺少 icon_256.png(256×256 PNG,其他尺寸可省略,由它縮小產生)。`);
|
|
91
|
+
return drawn;
|
|
57
92
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
93
|
+
function readIconFile(path) {
|
|
94
|
+
const image = readPng(path, basename(path));
|
|
95
|
+
if (image.width !== image.height)
|
|
96
|
+
throw new Error(`${basename(path)}:尺寸 ${image.width}×${image.height} 不是正方形。`);
|
|
97
|
+
if (image.width < 256)
|
|
98
|
+
throw new Error(`${basename(path)}:尺寸 ${image.width}×${image.height},至少 256×256。`);
|
|
99
|
+
return new Map([[256, scale(image, 256)]]);
|
|
100
|
+
}
|
|
101
|
+
function render(drawn, source) {
|
|
102
|
+
const big = drawn.get(256);
|
|
103
|
+
const window = new Map();
|
|
104
|
+
for (const size of WINDOW_ICON_SIZES) {
|
|
105
|
+
const png = encodePng(size, size, scale(drawn.get(size) ?? big, size).rgba);
|
|
106
|
+
if (png.length > MAX_ICON_BYTES) {
|
|
107
|
+
throw new Error(`${size}×${size} 圖示編碼後 ${png.length} bytes,超過 package source 的 256 KiB 上限;請減少雜訊或細節。`);
|
|
108
|
+
}
|
|
109
|
+
window.set(size, png);
|
|
66
110
|
}
|
|
111
|
+
return { source, window, packageIcon: window.get(64), packageIcon256: window.get(256), solid: isSolid(big) };
|
|
112
|
+
}
|
|
113
|
+
/** A project icon: one square PNG of at least 256 px, or a directory of icon_<size>.png. */
|
|
114
|
+
export function renderPackageIcons(path) {
|
|
115
|
+
if (!existsSync(path))
|
|
116
|
+
throw new Error(`找不到圖示 ${basename(path)}。`);
|
|
117
|
+
return render(statSync(path).isDirectory() ? readIconDirectory(path) : readIconFile(path), "custom");
|
|
118
|
+
}
|
|
119
|
+
let defaults;
|
|
120
|
+
/** The SDK default icon, for projects that set no `icon`. */
|
|
121
|
+
export function defaultPackageIcons() {
|
|
122
|
+
defaults ??= render(readIconDirectory(DEFAULT_ICON_DIR), "default");
|
|
123
|
+
return defaults;
|
|
124
|
+
}
|
|
125
|
+
/** The project's icon when it sets one, otherwise the SDK default. */
|
|
126
|
+
export function packageIcons(icon) {
|
|
127
|
+
return icon ? renderPackageIcons(icon) : defaultPackageIcons();
|
|
128
|
+
}
|
|
129
|
+
export function iconReport(root, icon) {
|
|
130
|
+
const icons = packageIcons(icon);
|
|
131
|
+
return { source: icons.source, ...(icon ? { path: relative(root, icon) } : {}), solid: icons.solid };
|
|
132
|
+
}
|
|
133
|
+
function writeIcon(path, png) {
|
|
134
|
+
writeFileSync(path, png);
|
|
135
|
+
chmodSync(path, 0o644);
|
|
136
|
+
}
|
|
137
|
+
export function writeDsmWindowIcons(uiDir, icons) {
|
|
138
|
+
const imagesDir = join(uiDir, "images");
|
|
139
|
+
mkdirSync(imagesDir, { recursive: true });
|
|
140
|
+
for (const [size, png] of icons.window)
|
|
141
|
+
writeIcon(join(imagesDir, `app_${size}.png`), png);
|
|
142
|
+
}
|
|
143
|
+
export function writePackageCenterIcons(spkRoot, icons) {
|
|
144
|
+
writeIcon(join(spkRoot, "PACKAGE_ICON.PNG"), icons.packageIcon);
|
|
145
|
+
writeIcon(join(spkRoot, "PACKAGE_ICON_256.PNG"), icons.packageIcon256);
|
|
67
146
|
}
|
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
"broadwell",
|
|
8
8
|
"broadwellnk",
|
|
9
9
|
"broadwellnkv2",
|
|
10
|
+
"broadwellntb",
|
|
10
11
|
"broadwellntbap",
|
|
11
12
|
"bromolow",
|
|
12
13
|
"cedarview",
|
|
14
|
+
"coffeelake",
|
|
13
15
|
"denverton",
|
|
14
16
|
"epyc7002",
|
|
15
17
|
"epyc7003",
|
|
@@ -23,13 +25,14 @@
|
|
|
23
25
|
"purley",
|
|
24
26
|
"r1000",
|
|
25
27
|
"r1000nk",
|
|
28
|
+
"skylaked",
|
|
26
29
|
"v1000",
|
|
27
30
|
"v1000nk"
|
|
28
31
|
],
|
|
29
32
|
"platformOnly": []
|
|
30
33
|
},
|
|
31
34
|
"aarch64": {
|
|
32
|
-
"members": ["armada37xx", "rtd1296", "rtd1619b"],
|
|
35
|
+
"members": ["armada37xx", "rtd1296", "rtd1619", "rtd1619b"],
|
|
33
36
|
"platformOnly": []
|
|
34
37
|
},
|
|
35
38
|
"armv7": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type CommandRunner } from "../standalone/build.js";
|
|
2
2
|
import type { AppContract } from "../types.js";
|
|
3
|
-
import {
|
|
3
|
+
import type { NativeArchFamily } from "./dsm-arch.js";
|
|
4
4
|
export { gatewayEntryPath } from "../contract/web-launch.js";
|
|
5
5
|
/** Where the gateway CGI sits in the package payload; DSM serves `ui/` under /webman/3rdparty/<package>/. */
|
|
6
6
|
export declare const GATEWAY_CGI_PAYLOAD = "ui/cgi/gateway.cgi";
|
|
@@ -4,7 +4,7 @@ import { checkCgiBinary } from "../standalone/binary.js";
|
|
|
4
4
|
import { runCommand } from "../standalone/build.js";
|
|
5
5
|
import { resolvedWebAccess } from "../contract/web-launch.js";
|
|
6
6
|
import { templatesRoot } from "../template-paths.js";
|
|
7
|
-
import {
|
|
7
|
+
import { PINNED_GO_HINT, pinnedGoBuildEnv, pinnedGoProblem } from "./go-toolchain.js";
|
|
8
8
|
export { gatewayEntryPath } from "../contract/web-launch.js";
|
|
9
9
|
/** Where the gateway CGI sits in the package payload; DSM serves `ui/` under /webman/3rdparty/<package>/. */
|
|
10
10
|
export const GATEWAY_CGI_PAYLOAD = "ui/cgi/gateway.cgi";
|
|
@@ -29,18 +29,22 @@ export function buildGatewayBinary(input) {
|
|
|
29
29
|
const ldflags = ["-s", "-w", "-buildid=", `-X main.packageID=${input.packageId}`, `-X 'main.configJSON=${JSON.stringify(input.config)}'`].join(" ");
|
|
30
30
|
const result = (input.run ?? runCommand)("go", ["build", "-trimpath", "-buildvcs=false", "-ldflags", ldflags, "-o", out, "."], {
|
|
31
31
|
cwd: join(templatesRoot(), "_gateway"),
|
|
32
|
-
env: { ...process.env, ...
|
|
32
|
+
env: { ...process.env, ...pinnedGoBuildEnv(input.arch) },
|
|
33
33
|
});
|
|
34
34
|
if (result.missing) {
|
|
35
|
-
return { error: "GATEWAY_GO_MISSING", detail:
|
|
35
|
+
return { error: "GATEWAY_GO_MISSING", detail: `找不到 go 指令。DSM 驗證閘道需要 Go 1.21 以上版本(https://go.dev/dl/)才能建置。${PINNED_GO_HINT}` };
|
|
36
36
|
}
|
|
37
37
|
if (result.status !== 0) {
|
|
38
|
-
return { error: "GATEWAY_BUILD_FAILED", detail: `go build(${input.arch}
|
|
38
|
+
return { error: "GATEWAY_BUILD_FAILED", detail: `go build(${input.arch})失敗。${PINNED_GO_HINT}\n${result.stderr}` };
|
|
39
39
|
}
|
|
40
40
|
const problem = checkCgiBinary(input.work, out, input.arch);
|
|
41
41
|
if (problem) {
|
|
42
42
|
return { error: "GATEWAY_BUILD_FAILED", detail: problem.message };
|
|
43
43
|
}
|
|
44
|
+
const toolchainProblem = pinnedGoProblem(out);
|
|
45
|
+
if (toolchainProblem) {
|
|
46
|
+
return { error: "GATEWAY_BUILD_FAILED", detail: toolchainProblem };
|
|
47
|
+
}
|
|
44
48
|
return { binary: out };
|
|
45
49
|
}
|
|
46
50
|
/** Puts the CGI (mode 0755) and its readable configuration into the package payload. */
|
|
@@ -11,7 +11,8 @@ import { buildGatewayBinary, gatewayConfig, writeGatewayPayload } from "./gatewa
|
|
|
11
11
|
import { classifyComposeRoute, loadCompose } from "../compose/analyze.js";
|
|
12
12
|
import { scanComposePrivilege } from "../compose/privilege.js";
|
|
13
13
|
import { validateProject } from "../validate.js";
|
|
14
|
-
import { writeDsmWindowIcons, writePackageCenterIcons } from "./dsm-icons.js";
|
|
14
|
+
import { packageIcons, writeDsmWindowIcons, writePackageCenterIcons } from "./dsm-icons.js";
|
|
15
|
+
import { contractIconPath } from "../contract/icon.js";
|
|
15
16
|
import { infoArchValue, isContainerArchFamily } from "./dsm-arch.js";
|
|
16
17
|
import { assertContainerMinDsm, assertPortalMinDsm, resolveMinDsm } from "./dsm-version.js";
|
|
17
18
|
import { createReproducibleTar } from "./reproducible-archive.js";
|
|
@@ -22,6 +23,9 @@ import { renderInstallWizardFiles } from "./install-wizard.js";
|
|
|
22
23
|
import { contractPortal, PORTAL_JSON_RELPATH, PORTAL_MUSTACHE_RELPATH, renderPortalJson, renderPortalLifecycle, renderPortalMustache, renderPortalWizardStep } from "../portal/package.js";
|
|
23
24
|
import { renderWizardFiles, writeWizardUiFiles } from "./wizard-i18n.js";
|
|
24
25
|
import { sharedFolderCombobox } from "./share-combobox.js";
|
|
26
|
+
import { writeUiTexts } from "./ui-texts.js";
|
|
27
|
+
import { DSM_LANGS, LOCALES } from "../standalone/i18n.js";
|
|
28
|
+
import { contractAppTitle } from "../contract/i18n.js";
|
|
25
29
|
import { renderHeadlessResourceMarker, renderResourceConfig, renderResourceWorkerStartStopStatus, } from "./resource-worker.js";
|
|
26
30
|
const FILE_MODE = 0o644;
|
|
27
31
|
const SCRIPT_MODE = 0o755;
|
|
@@ -89,6 +93,7 @@ export async function buildSpk(options) {
|
|
|
89
93
|
const isDynamicRoute = route?.route === "dynamic";
|
|
90
94
|
const isResourceWorkerRoute = route?.route === "static-zero-config" ||
|
|
91
95
|
isDynamicRoute;
|
|
96
|
+
const icons = packageIcons(contractIconPath(contract, options.project.root));
|
|
92
97
|
const staging = mkdtempSync(join(tmpdir(), "syno-spk-"));
|
|
93
98
|
const gatewayWork = mkdtempSync(join(tmpdir(), "syno-gateway-"));
|
|
94
99
|
try {
|
|
@@ -108,7 +113,7 @@ export async function buildSpk(options) {
|
|
|
108
113
|
gatewayBinary = built.binary;
|
|
109
114
|
}
|
|
110
115
|
writeFileSync(join(staging, "INFO"), renderInfo(contract, options.archFamily, isResourceWorkerRoute), "utf8");
|
|
111
|
-
writePackageCenterIcons(staging);
|
|
116
|
+
writePackageCenterIcons(staging, icons);
|
|
112
117
|
mkdirSync(join(staging, "conf"), { recursive: true });
|
|
113
118
|
writeFileSync(join(staging, "conf", "privilege"), renderPrivilege(), "utf8");
|
|
114
119
|
writeFileSync(join(staging, "privilege"), renderPrivilege(), "utf8");
|
|
@@ -177,7 +182,8 @@ export async function buildSpk(options) {
|
|
|
177
182
|
const uiDir = join(staging, "package", "ui");
|
|
178
183
|
mkdirSync(uiDir, { recursive: true });
|
|
179
184
|
writeFileSync(join(uiDir, "config"), renderDsmWindowConfig(contract), "utf8");
|
|
180
|
-
writeDsmWindowIcons(uiDir);
|
|
185
|
+
writeDsmWindowIcons(uiDir, icons);
|
|
186
|
+
writeUiTexts(uiDir, contractAppTitle(contract));
|
|
181
187
|
}
|
|
182
188
|
const portal = contractPortal(contract);
|
|
183
189
|
if (portal) {
|
|
@@ -223,6 +229,14 @@ function renderInfo(contract, archFamily, isResourceWorker = false) {
|
|
|
223
229
|
add("displayname", String(contract.displayname));
|
|
224
230
|
add("description", String(contract.description));
|
|
225
231
|
add("maintainer", String(contract.maintainer));
|
|
232
|
+
const info = contractAppTitle(contract).info;
|
|
233
|
+
for (const locale of LOCALES) {
|
|
234
|
+
const text = info?.[locale];
|
|
235
|
+
if (text?.displayname)
|
|
236
|
+
add(`displayname_${DSM_LANGS[locale]}`, text.displayname);
|
|
237
|
+
if (text?.description)
|
|
238
|
+
add(`description_${DSM_LANGS[locale]}`, text.description);
|
|
239
|
+
}
|
|
226
240
|
for (const [key, value] of metadataInfoFields(contract.metadata))
|
|
227
241
|
add(key, value);
|
|
228
242
|
add("arch", archFamily === undefined ? "noarch" : infoArchValue(archFamily));
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type NativeArchFamily } from "./dsm-arch.js";
|
|
2
|
+
/**
|
|
3
|
+
* The Go release that compiles every binary the CLI ships from its own
|
|
4
|
+
* templates, so their digests do not depend on the builder's Go. Each
|
|
5
|
+
* template go.mod names it in its `toolchain` line.
|
|
6
|
+
*/
|
|
7
|
+
export declare const PINNED_GO_TOOLCHAIN = "go1.26.9";
|
|
8
|
+
/**
|
|
9
|
+
* The `go build` environment for a binary the CLI ships from its own templates
|
|
10
|
+
* (executor, lifecycle helper, daemon controller, gateway). GOTOOLCHAIN names
|
|
11
|
+
* the exact release, overriding `local` or `auto`: Go 1.21+ runs it from PATH
|
|
12
|
+
* or the module cache, or downloads it, and fails when it cannot.
|
|
13
|
+
*/
|
|
14
|
+
export declare function pinnedGoBuildEnv(family: NativeArchFamily): Record<string, string>;
|
|
15
|
+
/** The Go release recorded in a binary's build info (Go 1.18+ layout), or undefined. */
|
|
16
|
+
export declare function goBinaryVersion(file: string): string | undefined;
|
|
17
|
+
/** Why a CLI-owned binary cannot ship: it was not built by the pinned Go release. */
|
|
18
|
+
export declare function pinnedGoProblem(file: string): string | undefined;
|
|
19
|
+
/** Appended to a failed CLI-owned `go build`: what the build needs. */
|
|
20
|
+
export declare const PINNED_GO_HINT = "These binaries need go1.26.9: Go 1.21 or later downloads it automatically; offline, install go1.26.9 on PATH or allow GOPROXY to serve golang.org/toolchain.";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { goBuildEnv } from "./dsm-arch.js";
|
|
3
|
+
/**
|
|
4
|
+
* The Go release that compiles every binary the CLI ships from its own
|
|
5
|
+
* templates, so their digests do not depend on the builder's Go. Each
|
|
6
|
+
* template go.mod names it in its `toolchain` line.
|
|
7
|
+
*/
|
|
8
|
+
export const PINNED_GO_TOOLCHAIN = "go1.26.9";
|
|
9
|
+
/**
|
|
10
|
+
* The `go build` environment for a binary the CLI ships from its own templates
|
|
11
|
+
* (executor, lifecycle helper, daemon controller, gateway). GOTOOLCHAIN names
|
|
12
|
+
* the exact release, overriding `local` or `auto`: Go 1.21+ runs it from PATH
|
|
13
|
+
* or the module cache, or downloads it, and fails when it cannot.
|
|
14
|
+
*/
|
|
15
|
+
export function pinnedGoBuildEnv(family) {
|
|
16
|
+
return { ...goBuildEnv(family), GOTOOLCHAIN: PINNED_GO_TOOLCHAIN, GOFLAGS: "", GOEXPERIMENT: "", GOWORK: "off", GOAMD64: "v1", GOARM64: "v8.0" };
|
|
17
|
+
}
|
|
18
|
+
const BUILD_INFO_MAGIC = Buffer.from("\xff Go buildinf:", "latin1");
|
|
19
|
+
const BUILD_INFO_INLINE_STRINGS = 0x2;
|
|
20
|
+
/** The Go release recorded in a binary's build info (Go 1.18+ layout), or undefined. */
|
|
21
|
+
export function goBinaryVersion(file) {
|
|
22
|
+
const bytes = readFileSync(file);
|
|
23
|
+
const start = bytes.indexOf(BUILD_INFO_MAGIC);
|
|
24
|
+
if (start < 0 || !(bytes[start + 15] & BUILD_INFO_INLINE_STRINGS))
|
|
25
|
+
return undefined;
|
|
26
|
+
let length = 0, shift = 0, at = start + 32;
|
|
27
|
+
for (;;) {
|
|
28
|
+
const byte = bytes[at++];
|
|
29
|
+
if (byte === undefined || shift > 28)
|
|
30
|
+
return undefined;
|
|
31
|
+
length |= (byte & 0x7f) << shift;
|
|
32
|
+
shift += 7;
|
|
33
|
+
if (byte < 0x80)
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
return bytes.subarray(at, at + length).toString("latin1") || undefined;
|
|
37
|
+
}
|
|
38
|
+
/** Why a CLI-owned binary cannot ship: it was not built by the pinned Go release. */
|
|
39
|
+
export function pinnedGoProblem(file) {
|
|
40
|
+
const version = goBinaryVersion(file);
|
|
41
|
+
return version === PINNED_GO_TOOLCHAIN ? undefined : `${file} was built by ${version ?? "an unknown Go release"}, not ${PINNED_GO_TOOLCHAIN}.`;
|
|
42
|
+
}
|
|
43
|
+
/** Appended to a failed CLI-owned `go build`: what the build needs. */
|
|
44
|
+
export const PINNED_GO_HINT = `These binaries need ${PINNED_GO_TOOLCHAIN}: Go 1.21 or later downloads it automatically; offline, install ${PINNED_GO_TOOLCHAIN} on PATH or allow GOPROXY to serve golang.org/toolchain.`;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Just enough PNG for package icons, without an image dependency: decode any
|
|
3
|
+
* non-interlaced PNG to 8-bit RGBA, and encode 8-bit RGBA deterministically
|
|
4
|
+
* with only the critical chunks.
|
|
5
|
+
*/
|
|
6
|
+
export interface RgbaImage {
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
/** Row-major, straight (not premultiplied) alpha. */
|
|
10
|
+
rgba: Buffer;
|
|
11
|
+
}
|
|
12
|
+
export declare function decodePng(bytes: Buffer): RgbaImage;
|
|
13
|
+
/** 8-bit RGBA, adaptive per-row filters, no ancillary chunks: same pixels, same bytes. */
|
|
14
|
+
export declare function encodePng(width: number, height: number, rgba: Buffer): Buffer;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { crc32, deflateSync, inflateSync } from "node:zlib";
|
|
2
|
+
const SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
3
|
+
const MAX_SIDE = 4096;
|
|
4
|
+
const CHANNELS = { 0: 1, 2: 3, 3: 1, 4: 2, 6: 4 };
|
|
5
|
+
const DEPTHS = { 0: [1, 2, 4, 8, 16], 2: [8, 16], 3: [1, 2, 4, 8], 4: [8, 16], 6: [8, 16] };
|
|
6
|
+
function corrupt(why) {
|
|
7
|
+
throw new Error(`PNG 損壞:${why}`);
|
|
8
|
+
}
|
|
9
|
+
function chunk(type, data) {
|
|
10
|
+
const head = Buffer.alloc(8);
|
|
11
|
+
head.writeUInt32BE(data.length);
|
|
12
|
+
head.write(type, 4, "ascii");
|
|
13
|
+
const crc = Buffer.alloc(4);
|
|
14
|
+
crc.writeUInt32BE(crc32(data, crc32(head.subarray(4))));
|
|
15
|
+
return Buffer.concat([head, data, crc]);
|
|
16
|
+
}
|
|
17
|
+
function paeth(a, b, c) {
|
|
18
|
+
const p = a + b - c;
|
|
19
|
+
const pa = Math.abs(p - a), pb = Math.abs(p - b), pc = Math.abs(p - c);
|
|
20
|
+
return pa <= pb && pa <= pc ? a : pb <= pc ? b : c;
|
|
21
|
+
}
|
|
22
|
+
export function decodePng(bytes) {
|
|
23
|
+
if (bytes.length < 8 || !bytes.subarray(0, 8).equals(SIGNATURE))
|
|
24
|
+
throw new Error("不是 PNG 檔(檔頭簽章不符)。");
|
|
25
|
+
let offset = 8;
|
|
26
|
+
let header;
|
|
27
|
+
let palette, alpha, ended = false;
|
|
28
|
+
const data = [];
|
|
29
|
+
while (!ended) {
|
|
30
|
+
if (offset + 12 > bytes.length)
|
|
31
|
+
corrupt("檔案被截斷");
|
|
32
|
+
const length = bytes.readUInt32BE(offset);
|
|
33
|
+
const type = bytes.toString("latin1", offset + 4, offset + 8);
|
|
34
|
+
const end = offset + 12 + length;
|
|
35
|
+
if (end > bytes.length)
|
|
36
|
+
corrupt("檔案被截斷");
|
|
37
|
+
const body = bytes.subarray(offset + 8, offset + 8 + length);
|
|
38
|
+
if (crc32(body, crc32(bytes.subarray(offset + 4, offset + 8))) !== bytes.readUInt32BE(end - 4))
|
|
39
|
+
corrupt(`${type} CRC 不符`);
|
|
40
|
+
if (!header && type !== "IHDR")
|
|
41
|
+
corrupt("第一個 chunk 必須是 IHDR");
|
|
42
|
+
if (type === "IHDR") {
|
|
43
|
+
if (header || length !== 13)
|
|
44
|
+
corrupt("IHDR 無效");
|
|
45
|
+
header = { width: body.readUInt32BE(0), height: body.readUInt32BE(4), depth: body[8], type: body[9] };
|
|
46
|
+
if (!DEPTHS[header.type]?.includes(header.depth) || body[10] !== 0 || body[11] !== 0)
|
|
47
|
+
corrupt("不支援的色彩型態或位元深度");
|
|
48
|
+
if (body[12] !== 0)
|
|
49
|
+
throw new Error("不支援交錯(interlaced)PNG;請另存為非交錯 PNG。");
|
|
50
|
+
if (header.width < 1 || header.height < 1 || header.width > MAX_SIDE || header.height > MAX_SIDE) {
|
|
51
|
+
throw new Error(`PNG 尺寸 ${header.width}×${header.height} 超出範圍(每邊 1–${MAX_SIDE} px)。`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
else if (type === "PLTE")
|
|
55
|
+
palette = body;
|
|
56
|
+
else if (type === "tRNS")
|
|
57
|
+
alpha = body;
|
|
58
|
+
else if (type === "IDAT")
|
|
59
|
+
data.push(body);
|
|
60
|
+
else if (type === "IEND")
|
|
61
|
+
ended = true;
|
|
62
|
+
offset = end;
|
|
63
|
+
}
|
|
64
|
+
if (!header || !data.length)
|
|
65
|
+
corrupt("缺少影像資料");
|
|
66
|
+
const { width, height, depth, type } = header;
|
|
67
|
+
if (type === 3 && (!palette || palette.length % 3 !== 0))
|
|
68
|
+
corrupt("調色盤影像缺少 PLTE");
|
|
69
|
+
const bits = CHANNELS[type] * depth;
|
|
70
|
+
const bpp = Math.max(1, bits >> 3);
|
|
71
|
+
const stride = Math.ceil((width * bits) / 8);
|
|
72
|
+
let raw;
|
|
73
|
+
try {
|
|
74
|
+
raw = inflateSync(Buffer.concat(data), { maxOutputLength: (stride + 1) * height + 1 });
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
corrupt("影像資料無法解壓縮");
|
|
78
|
+
}
|
|
79
|
+
if (raw.length !== (stride + 1) * height)
|
|
80
|
+
corrupt("影像資料長度不符");
|
|
81
|
+
const prev = Buffer.alloc(stride), line = Buffer.alloc(stride);
|
|
82
|
+
const rgba = Buffer.alloc(width * height * 4);
|
|
83
|
+
const sample = (x, channel) => {
|
|
84
|
+
if (depth === 16)
|
|
85
|
+
return line[(x * CHANNELS[type] + channel) * 2];
|
|
86
|
+
if (depth === 8)
|
|
87
|
+
return line[x * CHANNELS[type] + channel];
|
|
88
|
+
const bit = x * depth;
|
|
89
|
+
return (line[bit >> 3] >> (8 - depth - (bit & 7))) & ((1 << depth) - 1);
|
|
90
|
+
};
|
|
91
|
+
const scale = depth < 8 ? 255 / ((1 << depth) - 1) : 1;
|
|
92
|
+
for (let y = 0; y < height; y++) {
|
|
93
|
+
const row = raw.subarray(y * (stride + 1) + 1, (y + 1) * (stride + 1));
|
|
94
|
+
const filter = raw[y * (stride + 1)];
|
|
95
|
+
for (let i = 0; i < stride; i++) {
|
|
96
|
+
const a = i >= bpp ? line[i - bpp] : 0, b = prev[i], c = i >= bpp ? prev[i - bpp] : 0;
|
|
97
|
+
const predictor = filter === 0 ? 0 : filter === 1 ? a : filter === 2 ? b : filter === 3 ? (a + b) >> 1 : filter === 4 ? paeth(a, b, c) : corrupt("未知的列濾波");
|
|
98
|
+
line[i] = (row[i] + predictor) & 0xff;
|
|
99
|
+
}
|
|
100
|
+
for (let x = 0; x < width; x++) {
|
|
101
|
+
const out = (y * width + x) * 4;
|
|
102
|
+
if (type === 3) {
|
|
103
|
+
const index = sample(x, 0);
|
|
104
|
+
if (index * 3 + 2 >= palette.length)
|
|
105
|
+
corrupt("調色盤索引超出範圍");
|
|
106
|
+
palette.copy(rgba, out, index * 3, index * 3 + 3);
|
|
107
|
+
rgba[out + 3] = alpha && index < alpha.length ? alpha[index] : 255;
|
|
108
|
+
}
|
|
109
|
+
else if (type === 0 || type === 4) {
|
|
110
|
+
const grey = Math.round(sample(x, 0) * scale);
|
|
111
|
+
rgba.fill(grey, out, out + 3);
|
|
112
|
+
rgba[out + 3] = type === 4 ? sample(x, 1) : 255;
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
for (let channel = 0; channel < 3; channel++)
|
|
116
|
+
rgba[out + channel] = sample(x, channel);
|
|
117
|
+
rgba[out + 3] = type === 6 ? sample(x, 3) : 255;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
line.copy(prev);
|
|
121
|
+
}
|
|
122
|
+
return { width, height, rgba };
|
|
123
|
+
}
|
|
124
|
+
/** 8-bit RGBA, adaptive per-row filters, no ancillary chunks: same pixels, same bytes. */
|
|
125
|
+
export function encodePng(width, height, rgba) {
|
|
126
|
+
const stride = width * 4;
|
|
127
|
+
const out = Buffer.alloc((stride + 1) * height);
|
|
128
|
+
const zero = Buffer.alloc(stride);
|
|
129
|
+
const candidate = Buffer.alloc(stride);
|
|
130
|
+
for (let y = 0; y < height; y++) {
|
|
131
|
+
const line = rgba.subarray(y * stride, (y + 1) * stride);
|
|
132
|
+
const prev = y ? rgba.subarray((y - 1) * stride, y * stride) : zero;
|
|
133
|
+
let bestScore = Infinity;
|
|
134
|
+
for (let filter = 0; filter <= 4; filter++) {
|
|
135
|
+
let score = 0;
|
|
136
|
+
for (let i = 0; i < stride; i++) {
|
|
137
|
+
const a = i >= 4 ? line[i - 4] : 0, b = prev[i], c = i >= 4 ? prev[i - 4] : 0;
|
|
138
|
+
const predictor = filter === 0 ? 0 : filter === 1 ? a : filter === 2 ? b : filter === 3 ? (a + b) >> 1 : paeth(a, b, c);
|
|
139
|
+
const value = (line[i] - predictor) & 0xff;
|
|
140
|
+
candidate[i] = value;
|
|
141
|
+
score += value < 128 ? value : 256 - value;
|
|
142
|
+
}
|
|
143
|
+
if (score < bestScore) {
|
|
144
|
+
bestScore = score;
|
|
145
|
+
out[y * (stride + 1)] = filter;
|
|
146
|
+
candidate.copy(out, y * (stride + 1) + 1);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const ihdr = Buffer.alloc(13);
|
|
151
|
+
ihdr.writeUInt32BE(width, 0);
|
|
152
|
+
ihdr.writeUInt32BE(height, 4);
|
|
153
|
+
ihdr[8] = 8;
|
|
154
|
+
ihdr[9] = 6;
|
|
155
|
+
return Buffer.concat([
|
|
156
|
+
SIGNATURE,
|
|
157
|
+
chunk("IHDR", ihdr),
|
|
158
|
+
chunk("IDAT", deflateSync(out, { level: 9 })),
|
|
159
|
+
chunk("IEND", Buffer.alloc(0)),
|
|
160
|
+
]);
|
|
161
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type InfoTranslations } from "../standalone/i18n.js";
|
|
2
|
+
/** The English INFO text and its translations, which also name the app in DSM. */
|
|
3
|
+
export interface AppTitle {
|
|
4
|
+
displayname: string;
|
|
5
|
+
description: string;
|
|
6
|
+
info?: InfoTranslations;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* `ui/config` title fields. With translations they are `section:key`
|
|
10
|
+
* references DSM resolves from `ui/texts/<lang>/strings` for the main menu
|
|
11
|
+
* and the window title; without, the literal English text as before.
|
|
12
|
+
*/
|
|
13
|
+
export declare function uiTitleFields(app: AppTitle): {
|
|
14
|
+
title: string;
|
|
15
|
+
desc: string;
|
|
16
|
+
preloadTexts?: string[];
|
|
17
|
+
};
|
|
18
|
+
/** `ui/texts/<dsm lang>/strings` for English and every translated language; none without translations. */
|
|
19
|
+
export declare function renderUiTexts(app: AppTitle): Map<string, string>;
|
|
20
|
+
export declare function writeUiTexts(uiRoot: string, app: AppTitle): void;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { DSM_LANGS, LOCALES } from "../standalone/i18n.js";
|
|
4
|
+
const SECTION = "synopkgland";
|
|
5
|
+
const TITLE = `${SECTION}:app_title`;
|
|
6
|
+
const DESC = `${SECTION}:app_desc`;
|
|
7
|
+
function translated(app) {
|
|
8
|
+
return Object.values(app.info ?? {}).some((text) => text?.displayname || text?.description);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* `ui/config` title fields. With translations they are `section:key`
|
|
12
|
+
* references DSM resolves from `ui/texts/<lang>/strings` for the main menu
|
|
13
|
+
* and the window title; without, the literal English text as before.
|
|
14
|
+
*/
|
|
15
|
+
export function uiTitleFields(app) {
|
|
16
|
+
return translated(app)
|
|
17
|
+
? { title: TITLE, desc: DESC, preloadTexts: [TITLE, DESC] }
|
|
18
|
+
: { title: app.displayname, desc: app.description };
|
|
19
|
+
}
|
|
20
|
+
/** `ui/texts/<dsm lang>/strings` for English and every translated language; none without translations. */
|
|
21
|
+
export function renderUiTexts(app) {
|
|
22
|
+
const files = new Map();
|
|
23
|
+
if (!translated(app))
|
|
24
|
+
return files;
|
|
25
|
+
for (const locale of LOCALES) {
|
|
26
|
+
const text = locale === "en" ? undefined : app.info?.[locale];
|
|
27
|
+
if (locale !== "en" && !text)
|
|
28
|
+
continue;
|
|
29
|
+
const title = text?.displayname ?? app.displayname;
|
|
30
|
+
const desc = text?.description ?? app.description;
|
|
31
|
+
files.set(DSM_LANGS[locale], `[${SECTION}]\napp_title="${title}"\napp_desc="${desc}"\n`);
|
|
32
|
+
}
|
|
33
|
+
return files;
|
|
34
|
+
}
|
|
35
|
+
export function writeUiTexts(uiRoot, app) {
|
|
36
|
+
for (const [lang, strings] of renderUiTexts(app)) {
|
|
37
|
+
mkdirSync(join(uiRoot, "texts", lang), { recursive: true });
|
|
38
|
+
const path = join(uiRoot, "texts", lang, "strings");
|
|
39
|
+
writeFileSync(path, strings, "utf8");
|
|
40
|
+
chmodSync(path, 0o644);
|
|
41
|
+
}
|
|
42
|
+
}
|