@platforma-sdk/package-builder-lib 1.1.0 → 1.2.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.
- package/dist/core/archive.cjs +3 -3
- package/dist/core/archive.cjs.map +1 -1
- package/dist/core/archive.js.map +1 -1
- package/dist/core/conda/builder.cjs +9 -4
- package/dist/core/conda/builder.cjs.map +1 -1
- package/dist/core/conda/builder.js +5 -1
- package/dist/core/conda/builder.js.map +1 -1
- package/dist/core/conda/validate-spec.cjs +2 -2
- package/dist/core/conda/validate-spec.cjs.map +1 -1
- package/dist/core/conda/validate-spec.js.map +1 -1
- package/dist/core/core.cjs +29 -8
- package/dist/core/core.cjs.map +1 -1
- package/dist/core/core.d.ts +6 -1
- package/dist/core/core.d.ts.map +1 -1
- package/dist/core/core.js +27 -6
- package/dist/core/core.js.map +1 -1
- package/dist/core/docker-conda.cjs +2 -2
- package/dist/core/docker-conda.cjs.map +1 -1
- package/dist/core/docker-conda.js.map +1 -1
- package/dist/core/docker-python.cjs +3 -2
- package/dist/core/docker-python.cjs.map +1 -1
- package/dist/core/docker-python.js +1 -1
- package/dist/core/docker-python.js.map +1 -1
- package/dist/core/docker.cjs +3 -3
- package/dist/core/docker.cjs.map +1 -1
- package/dist/core/docker.js.map +1 -1
- package/dist/core/envs.cjs +34 -0
- package/dist/core/envs.cjs.map +1 -1
- package/dist/core/envs.d.ts +8 -2
- package/dist/core/envs.d.ts.map +1 -1
- package/dist/core/envs.js +13 -1
- package/dist/core/envs.js.map +1 -1
- package/dist/core/package-info.cjs +59 -5
- package/dist/core/package-info.cjs.map +1 -1
- package/dist/core/package-info.d.ts +11 -2
- package/dist/core/package-info.d.ts.map +1 -1
- package/dist/core/package-info.js +60 -6
- package/dist/core/package-info.js.map +1 -1
- package/dist/core/paths.cjs +1 -1
- package/dist/core/paths.cjs.map +1 -1
- package/dist/core/paths.js +1 -1
- package/dist/core/paths.js.map +1 -1
- package/dist/core/resolver.cjs +7 -2
- package/dist/core/resolver.cjs.map +1 -1
- package/dist/core/resolver.js +1 -1
- package/dist/core/resolver.js.map +1 -1
- package/dist/core/schemas/artifacts.cjs +7 -1
- package/dist/core/schemas/artifacts.cjs.map +1 -1
- package/dist/core/schemas/artifacts.d.ts +1 -1
- package/dist/core/schemas/artifacts.d.ts.map +1 -1
- package/dist/core/schemas/artifacts.js +1 -1
- package/dist/core/schemas/artifacts.js.map +1 -1
- package/dist/core/schemas/entrypoints.cjs +4 -1
- package/dist/core/schemas/entrypoints.cjs.map +1 -1
- package/dist/core/schemas/entrypoints.d.ts +1 -1
- package/dist/core/schemas/entrypoints.d.ts.map +1 -1
- package/dist/core/schemas/entrypoints.js +1 -1
- package/dist/core/schemas/entrypoints.js.map +1 -1
- package/dist/core/schemas/sw-json.cjs +16 -1
- package/dist/core/schemas/sw-json.cjs.map +1 -1
- package/dist/core/schemas/sw-json.js +1 -1
- package/dist/core/schemas/sw-json.js.map +1 -1
- package/dist/core/storage.cjs +6 -2
- package/dist/core/storage.cjs.map +1 -1
- package/dist/core/storage.js +4 -1
- package/dist/core/storage.js.map +1 -1
- package/dist/core/sw-json-render.cjs +38 -7
- package/dist/core/sw-json-render.cjs.map +1 -1
- package/dist/core/sw-json-render.js +37 -8
- package/dist/core/sw-json-render.js.map +1 -1
- package/dist/core/util.cjs +33 -12
- package/dist/core/util.cjs.map +1 -1
- package/dist/core/util.d.ts +12 -7
- package/dist/core/util.d.ts.map +1 -1
- package/dist/core/util.js +17 -8
- package/dist/core/util.js.map +1 -1
- package/dist/defaults.cjs +18 -1
- package/dist/defaults.cjs.map +1 -1
- package/dist/defaults.d.ts +9 -3
- package/dist/defaults.d.ts.map +1 -1
- package/dist/defaults.js +13 -2
- package/dist/defaults.js.map +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/core/core.test.ts +17 -0
- package/src/core/core.ts +48 -8
- package/src/core/envs.ts +8 -0
- package/src/core/package-info.test.ts +162 -3
- package/src/core/package-info.ts +88 -3
- package/src/core/schemas/test-artifacts.ts +1 -1
- package/src/core/sw-json-render.test.ts +23 -0
- package/src/core/sw-json-render.ts +30 -2
- package/src/core/util.test.ts +52 -0
- package/src/core/util.ts +27 -7
- package/src/defaults.ts +19 -1
- package/src/index.ts +4 -0
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import { PackageInfo } from "./package-info";
|
|
2
2
|
import * as testArtifacts from "./schemas/test-artifacts";
|
|
3
|
-
import { createLogger } from "./util";
|
|
4
|
-
import
|
|
3
|
+
import { createLogger, isDevMode, producesRegistryDescriptor } from "./util";
|
|
4
|
+
import * as envs from "./envs";
|
|
5
|
+
import * as defaults from "../defaults";
|
|
6
|
+
import { test, expect, afterEach } from "vitest";
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
import path from "node:path";
|
|
5
10
|
|
|
6
11
|
test("PackageInfo loads correctly for single binary package", () => {
|
|
7
12
|
const l = createLogger("error");
|
|
8
13
|
|
|
9
14
|
const i = new PackageInfo(l, {
|
|
10
|
-
pkgJsonData: testArtifacts.
|
|
15
|
+
pkgJsonData: testArtifacts.SingleBinaryPackageJson,
|
|
11
16
|
});
|
|
12
17
|
|
|
13
18
|
const binary = i.getArtifact(testArtifacts.EPNameBinary, "binary");
|
|
@@ -80,3 +85,157 @@ test("PackageInfo considers version override", () => {
|
|
|
80
85
|
const java = i.getArtifact(testArtifacts.EPNameJava, "java");
|
|
81
86
|
expect(i.artifactVersion(java)).toEqual(customVersion);
|
|
82
87
|
});
|
|
88
|
+
|
|
89
|
+
test("content-addressable dev naming appends a content hash", () => {
|
|
90
|
+
const l = createLogger("error");
|
|
91
|
+
|
|
92
|
+
// Real package roots so the asset's "./src" content root resolves and can be hashed.
|
|
93
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pkg-info-dev-"));
|
|
94
|
+
const root2 = fs.mkdtempSync(path.join(os.tmpdir(), "pkg-info-dev-"));
|
|
95
|
+
const devAsset = (packageRoot: string): string => {
|
|
96
|
+
const pi = new PackageInfo(l, { packageRoot, pkgJsonData: testArtifacts.PackageJson });
|
|
97
|
+
pi.buildMode = "dev-local";
|
|
98
|
+
return pi.artifactVersion(pi.getArtifact(testArtifacts.EPNameAsset, "asset"));
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
fs.mkdirSync(path.join(root, "src"));
|
|
103
|
+
fs.writeFileSync(path.join(root, "src", "a.txt"), "hello");
|
|
104
|
+
|
|
105
|
+
const i = new PackageInfo(l, { packageRoot: root, pkgJsonData: testArtifacts.PackageJson });
|
|
106
|
+
const asset = i.getArtifact(testArtifacts.EPNameAsset, "asset");
|
|
107
|
+
const docker = i.getArtifact(testArtifacts.EPNameDocker, "docker");
|
|
108
|
+
|
|
109
|
+
// Release (default): version-derived, no suffix.
|
|
110
|
+
expect(i.artifactVersion(asset)).toEqual(testArtifacts.PackageVersion);
|
|
111
|
+
|
|
112
|
+
// Dev: a `-<12 hex>` content suffix, deterministic for unchanged content.
|
|
113
|
+
i.buildMode = "dev-local";
|
|
114
|
+
const dev1 = i.artifactVersion(asset);
|
|
115
|
+
expect(dev1).toMatch(new RegExp(`^${testArtifacts.PackageVersion}-[0-9a-f]{12}$`));
|
|
116
|
+
expect(i.artifactVersion(asset)).toEqual(dev1);
|
|
117
|
+
|
|
118
|
+
// Docker stays version-derived (already content-addressed via its image-ID tag).
|
|
119
|
+
expect(i.artifactVersion(docker)).toEqual(testArtifacts.PackageVersion);
|
|
120
|
+
|
|
121
|
+
// Reproducible: identical content at a different absolute location yields the same suffix
|
|
122
|
+
// (hash is over relative paths + file contents, sorted — not the build directory).
|
|
123
|
+
fs.mkdirSync(path.join(root2, "src"));
|
|
124
|
+
fs.writeFileSync(path.join(root2, "src", "a.txt"), "hello");
|
|
125
|
+
expect(devAsset(root2)).toEqual(dev1);
|
|
126
|
+
|
|
127
|
+
// Changed content ⟹ a different name (a fresh build over the changed tree).
|
|
128
|
+
fs.writeFileSync(path.join(root, "src", "b.txt"), "world");
|
|
129
|
+
expect(devAsset(root)).not.toEqual(dev1);
|
|
130
|
+
} finally {
|
|
131
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
132
|
+
fs.rmSync(root2, { recursive: true, force: true });
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("dev content hash: absent root yields no suffix; a pinned version still gets one", () => {
|
|
137
|
+
const l = createLogger("error");
|
|
138
|
+
const empty = fs.mkdtempSync(path.join(os.tmpdir(), "pkg-info-noroot-"));
|
|
139
|
+
const withSrc = fs.mkdtempSync(path.join(os.tmpdir(), "pkg-info-src-"));
|
|
140
|
+
try {
|
|
141
|
+
// No ./src on disk → the asset's content root is absent → no hash suffix, even in dev.
|
|
142
|
+
const noRoot = new PackageInfo(l, {
|
|
143
|
+
packageRoot: empty,
|
|
144
|
+
pkgJsonData: testArtifacts.PackageJson,
|
|
145
|
+
});
|
|
146
|
+
noRoot.buildMode = "dev-local";
|
|
147
|
+
expect(noRoot.artifactVersion(noRoot.getArtifact(testArtifacts.EPNameAsset, "asset"))).toEqual(
|
|
148
|
+
testArtifacts.PackageVersion,
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
// Present root + explicit --version override: dev naming stays content-addressable, so the
|
|
152
|
+
// pinned version still gets the -<hash> suffix (a same-name re-push would be silently stale).
|
|
153
|
+
fs.mkdirSync(path.join(withSrc, "src"));
|
|
154
|
+
fs.writeFileSync(path.join(withSrc, "src", "a.txt"), "hello");
|
|
155
|
+
const pinned = new PackageInfo(l, {
|
|
156
|
+
packageRoot: withSrc,
|
|
157
|
+
pkgJsonData: testArtifacts.PackageJson,
|
|
158
|
+
});
|
|
159
|
+
pinned.buildMode = "dev-local";
|
|
160
|
+
pinned.version = "9.9.9";
|
|
161
|
+
expect(pinned.artifactVersion(pinned.getArtifact(testArtifacts.EPNameAsset, "asset"))).toMatch(
|
|
162
|
+
/^9\.9\.9-[0-9a-f]{12}$/,
|
|
163
|
+
);
|
|
164
|
+
} finally {
|
|
165
|
+
fs.rmSync(empty, { recursive: true, force: true });
|
|
166
|
+
fs.rmSync(withSrc, { recursive: true, force: true });
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("dev content hash is memoized for the builder's lifetime", () => {
|
|
171
|
+
const l = createLogger("error");
|
|
172
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pkg-info-memo-"));
|
|
173
|
+
try {
|
|
174
|
+
fs.mkdirSync(path.join(root, "src"));
|
|
175
|
+
fs.writeFileSync(path.join(root, "src", "a.txt"), "hello");
|
|
176
|
+
const i = new PackageInfo(l, { packageRoot: root, pkgJsonData: testArtifacts.PackageJson });
|
|
177
|
+
i.buildMode = "dev-local";
|
|
178
|
+
const asset = i.getArtifact(testArtifacts.EPNameAsset, "asset");
|
|
179
|
+
const first = i.artifactVersion(asset);
|
|
180
|
+
// Mutate the tree; content roots are read once per builder, so the cached hash must not change.
|
|
181
|
+
fs.writeFileSync(path.join(root, "src", "b.txt"), "world");
|
|
182
|
+
expect(i.artifactVersion(asset)).toEqual(first);
|
|
183
|
+
} finally {
|
|
184
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("build-mode predicates split channel from descriptor shape", () => {
|
|
189
|
+
expect(isDevMode("dev-local")).toBe(true);
|
|
190
|
+
expect(isDevMode("dev-remote")).toBe(true);
|
|
191
|
+
expect(isDevMode("release")).toBe(false);
|
|
192
|
+
|
|
193
|
+
// dev-local is the only same-host mode: it does NOT build an archive / registry descriptor.
|
|
194
|
+
expect(producesRegistryDescriptor("dev-local")).toBe(false);
|
|
195
|
+
expect(producesRegistryDescriptor("dev-remote")).toBe(true);
|
|
196
|
+
expect(producesRegistryDescriptor("release")).toBe(true);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const devUploadEnv = envs.PL_DEV_BINARY_UPLOAD_URL;
|
|
200
|
+
const releaseUploadEnv = envs.PL_RELEASE_BINARY_UPLOAD_URL;
|
|
201
|
+
afterEach(() => {
|
|
202
|
+
delete process.env[devUploadEnv];
|
|
203
|
+
delete process.env[releaseUploadEnv];
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("dev channel flips the embedded binary registry name to midev/dev", () => {
|
|
207
|
+
const l = createLogger("error");
|
|
208
|
+
const i = new PackageInfo(l, { pkgJsonData: testArtifacts.SingleBinaryPackageJson });
|
|
209
|
+
const binary = i.getArtifact(testArtifacts.EPNameBinary, "binary");
|
|
210
|
+
|
|
211
|
+
// Release: keeps the artifact's resolved registry name (default platforma-open here).
|
|
212
|
+
const releaseName = i.artifactRegistrySettings(binary).name;
|
|
213
|
+
expect(releaseName).toEqual("platforma-open");
|
|
214
|
+
|
|
215
|
+
// Dev, built-in default: name is midev, upload routes to the built-in midev endpoint.
|
|
216
|
+
i.buildMode = "dev-remote";
|
|
217
|
+
const devDefault = i.artifactRegistrySettings(binary);
|
|
218
|
+
expect(devDefault.name).toEqual("midev");
|
|
219
|
+
expect(devDefault.storageURL).toEqual(defaults.DEV_BINARY_UPLOAD_TARGET);
|
|
220
|
+
|
|
221
|
+
// Dev with an explicit endpoint: name flips to dev and the upload URL routes there.
|
|
222
|
+
process.env[devUploadEnv] = "s3://my-bucket/dev?region=eu-central-1";
|
|
223
|
+
const dev = i.artifactRegistrySettings(binary);
|
|
224
|
+
expect(dev.name).toEqual("dev");
|
|
225
|
+
expect(dev.storageURL).toEqual("s3://my-bucket/dev?region=eu-central-1");
|
|
226
|
+
|
|
227
|
+
// dev-local channel behaves the same for naming.
|
|
228
|
+
i.buildMode = "dev-local";
|
|
229
|
+
expect(i.artifactRegistrySettings(binary).name).toEqual("dev");
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("release honors PL_RELEASE_BINARY_UPLOAD_URL without renaming", () => {
|
|
233
|
+
const l = createLogger("error");
|
|
234
|
+
const i = new PackageInfo(l, { pkgJsonData: testArtifacts.SingleBinaryPackageJson });
|
|
235
|
+
const binary = i.getArtifact(testArtifacts.EPNameBinary, "binary");
|
|
236
|
+
|
|
237
|
+
process.env[releaseUploadEnv] = "s3://release-bucket/pub?region=eu-central-1";
|
|
238
|
+
const rel = i.artifactRegistrySettings(binary);
|
|
239
|
+
expect(rel.name).toEqual("platforma-open");
|
|
240
|
+
expect(rel.storageURL).toEqual("s3://release-bucket/pub?region=eu-central-1");
|
|
241
|
+
});
|
package/src/core/package-info.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
3
4
|
import type winston from "winston";
|
|
4
5
|
import { z } from "zod/v4";
|
|
5
6
|
|
|
@@ -85,6 +86,9 @@ export class PackageInfo {
|
|
|
85
86
|
private readonly pkgJson: packageJson;
|
|
86
87
|
private _versionOverride: string | undefined;
|
|
87
88
|
|
|
89
|
+
/** Backing store for `Core.buildMode` (Core delegates its accessor here); gates content-addressable dev naming. */
|
|
90
|
+
public buildMode: util.BuildMode = "release";
|
|
91
|
+
|
|
88
92
|
constructor(
|
|
89
93
|
private logger: winston.Logger,
|
|
90
94
|
options?: {
|
|
@@ -499,7 +503,20 @@ export class PackageInfo {
|
|
|
499
503
|
return util.trimPrefix(this.pkgJson.name, "@") + "/" + artifact.id;
|
|
500
504
|
}
|
|
501
505
|
|
|
502
|
-
public artifactVersion(artifact: artifacts.anyArtifactType): string {
|
|
506
|
+
public artifactVersion(artifact: artifacts.withId<artifacts.anyArtifactType>): string {
|
|
507
|
+
const base = this.baseArtifactVersion(artifact);
|
|
508
|
+
|
|
509
|
+
if (util.isDevMode(this.buildMode) && artifact.type !== "docker") {
|
|
510
|
+
const hash = this.devContentHash(artifact);
|
|
511
|
+
if (hash) {
|
|
512
|
+
return `${base}-${hash}`;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return base;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
private baseArtifactVersion(artifact: artifacts.withId<artifacts.anyArtifactType>): string {
|
|
503
520
|
if (this._versionOverride) {
|
|
504
521
|
return this._versionOverride;
|
|
505
522
|
}
|
|
@@ -515,6 +532,58 @@ export class PackageInfo {
|
|
|
515
532
|
return this.pkgJson.version;
|
|
516
533
|
}
|
|
517
534
|
|
|
535
|
+
/**
|
|
536
|
+
* Short content hash of the artifact's content root(s), used as the dev version suffix.
|
|
537
|
+
*/
|
|
538
|
+
private readonly devContentHashCache = new Map<string, string | undefined>();
|
|
539
|
+
|
|
540
|
+
private devContentHash(
|
|
541
|
+
artifact: artifacts.withId<artifacts.anyArtifactType>,
|
|
542
|
+
): string | undefined {
|
|
543
|
+
// Content roots are stable for the lifetime of this builder, so hash each artifact once.
|
|
544
|
+
if (this.devContentHashCache.has(artifact.id)) {
|
|
545
|
+
return this.devContentHashCache.get(artifact.id);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const hash = this.computeDevContentHash(artifact);
|
|
549
|
+
this.devContentHashCache.set(artifact.id, hash);
|
|
550
|
+
return hash;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
private computeDevContentHash(
|
|
554
|
+
artifact: artifacts.withId<artifacts.anyArtifactType>,
|
|
555
|
+
): string | undefined {
|
|
556
|
+
const hasher = createHash("sha256");
|
|
557
|
+
let hashedAny = false;
|
|
558
|
+
|
|
559
|
+
let platforms: util.PlatformType[];
|
|
560
|
+
try {
|
|
561
|
+
// Sorted so the hash is independent of package.json root-key order.
|
|
562
|
+
platforms = this.artifactPlatforms(artifact).toSorted();
|
|
563
|
+
} catch {
|
|
564
|
+
return undefined; // no content roots at all — leave the version without a suffix
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
for (const platform of platforms) {
|
|
568
|
+
let root: string;
|
|
569
|
+
try {
|
|
570
|
+
root = this.artifactContentRoot(artifact, platform);
|
|
571
|
+
} catch {
|
|
572
|
+
continue; // platform without a defined/present root — skip
|
|
573
|
+
}
|
|
574
|
+
if (!fs.existsSync(root)) {
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
// Hash the platform name too, so the same files under a different platform yield a different
|
|
578
|
+
// hash.
|
|
579
|
+
hasher.update(platform);
|
|
580
|
+
util.hashDirSync(root, hasher);
|
|
581
|
+
hashedAny = true;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return hashedAny ? hasher.digest("hex").slice(0, 12) : undefined;
|
|
585
|
+
}
|
|
586
|
+
|
|
518
587
|
public artifactRegistrySettings(artifact: artifacts.anyArtifactType): artifacts.registry {
|
|
519
588
|
if (artifact.type === "docker") {
|
|
520
589
|
return {
|
|
@@ -559,6 +628,21 @@ export class PackageInfo {
|
|
|
559
628
|
result.downloadURL = downloadFrom;
|
|
560
629
|
}
|
|
561
630
|
|
|
631
|
+
// Dev channel: the embedded binary registry is the built-in `midev` uploading to its built-in
|
|
632
|
+
// endpoint, flipping to `dev` and `PL_DEV_BINARY_UPLOAD_URL` when that is set.
|
|
633
|
+
if (util.isDevMode(this.buildMode)) {
|
|
634
|
+
const devUpload = process.env[envs.PL_DEV_BINARY_UPLOAD_URL];
|
|
635
|
+
result.name = devUpload
|
|
636
|
+
? defaults.DEV_BIN_REGISTRY_NAME_OVERRIDDEN
|
|
637
|
+
: defaults.DEV_BIN_REGISTRY_NAME;
|
|
638
|
+
result.storageURL = devUpload ?? defaults.DEV_BINARY_UPLOAD_TARGET;
|
|
639
|
+
} else {
|
|
640
|
+
const releaseUpload = process.env[envs.PL_RELEASE_BINARY_UPLOAD_URL];
|
|
641
|
+
if (releaseUpload) {
|
|
642
|
+
result.storageURL = releaseUpload;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
562
646
|
if (result.downloadURL) {
|
|
563
647
|
const u = new URL(result.downloadURL, "file:/nonexistent"); // check download URL is valid URL
|
|
564
648
|
if (!["https:", "http:"].includes(u.protocol)) {
|
|
@@ -718,8 +802,9 @@ export class PackageInfo {
|
|
|
718
802
|
hasErrors = true;
|
|
719
803
|
}
|
|
720
804
|
|
|
721
|
-
const
|
|
722
|
-
const
|
|
805
|
+
const withId = { id: artifactID, ...artifact };
|
|
806
|
+
const name = this.artifactName(withId);
|
|
807
|
+
const version = this.artifactVersion(withId);
|
|
723
808
|
const uniqueName = `${name}-${version}`;
|
|
724
809
|
if (uniquePackageNames.has(uniqueName)) {
|
|
725
810
|
this.logger.error(
|
|
@@ -156,6 +156,29 @@ describe("Renderer tests", () => {
|
|
|
156
156
|
expect(javaDescriptor.docker!.cmd).toEqual(["java", "-jar", "hello.jar"]);
|
|
157
157
|
});
|
|
158
158
|
|
|
159
|
+
test("render no-software placeholder", () => {
|
|
160
|
+
const sw = new SwJsonRenderer(l, i);
|
|
161
|
+
|
|
162
|
+
// No artifact info written — placeholders need nothing built. A docker+binary pair collapses
|
|
163
|
+
// to a single placeholder for their shared origin entrypoint.
|
|
164
|
+
const epName = test_assets.EPNameJava;
|
|
165
|
+
const eps = new Map([
|
|
166
|
+
[docker.entrypointName(epName), i.getMainEntrypoint(docker.entrypointName(epName))],
|
|
167
|
+
[epName, i.getMainEntrypoint(epName)],
|
|
168
|
+
]);
|
|
169
|
+
const result = sw.renderPlaceholderEntrypoints(eps);
|
|
170
|
+
|
|
171
|
+
expect(result.size).toBe(1);
|
|
172
|
+
const descriptor = result.get(epName)!;
|
|
173
|
+
expect(descriptor.isDev).toBe(true);
|
|
174
|
+
expect(descriptor.binary).toEqual({
|
|
175
|
+
type: "binary",
|
|
176
|
+
registry: defaults.NO_SOFTWARE_PLACEHOLDER,
|
|
177
|
+
package: defaults.NO_SOFTWARE_PLACEHOLDER,
|
|
178
|
+
cmd: ["true"],
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
159
182
|
test("render docker indinvidual entrypoint", () => {
|
|
160
183
|
const dockerEpName = test_assets.EPNameDocker;
|
|
161
184
|
const dockerPkg = i.getArtifact(dockerEpName, "docker");
|
|
@@ -58,12 +58,12 @@ export class SwJsonRenderer {
|
|
|
58
58
|
throw util.CLIError(`Entrypoint ${epName} not found in result`);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
if (mode
|
|
61
|
+
if (util.isDevMode(mode)) {
|
|
62
62
|
info.isDev = true;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
const pkg = ep.artifact;
|
|
66
|
-
if (util.
|
|
66
|
+
if (!util.producesRegistryDescriptor(mode)) {
|
|
67
67
|
switch (pkg.type) {
|
|
68
68
|
case "docker": {
|
|
69
69
|
info.docker = this.renderDockerInfo(epName, ep, options?.requireAllArtifacts);
|
|
@@ -122,6 +122,31 @@ export class SwJsonRenderer {
|
|
|
122
122
|
return result;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
// One minimal `binary` placeholder per entrypoint, so a block builds, loads, and renders without
|
|
126
|
+
// any software; only executing it fails, against the sentinel registry. References are copied
|
|
127
|
+
// through by the caller, so they are skipped here.
|
|
128
|
+
public renderPlaceholderEntrypoints(
|
|
129
|
+
entrypoints: Map<string, entrypoint.Entrypoint>,
|
|
130
|
+
): Map<string, swJson.swJsonType> {
|
|
131
|
+
const result = new Map<string, swJson.swJsonType>();
|
|
132
|
+
for (const [epName, ep] of entrypoints.entries()) {
|
|
133
|
+
if (ep.type === "reference") continue;
|
|
134
|
+
const originEpName = docker.entrypointNameToOrigin(epName);
|
|
135
|
+
if (result.has(originEpName)) continue;
|
|
136
|
+
result.set(originEpName, {
|
|
137
|
+
id: { package: this.pkgInfo.packageName, name: originEpName },
|
|
138
|
+
isDev: true,
|
|
139
|
+
binary: {
|
|
140
|
+
type: "binary",
|
|
141
|
+
registry: defaults.NO_SOFTWARE_PLACEHOLDER,
|
|
142
|
+
package: defaults.NO_SOFTWARE_PLACEHOLDER,
|
|
143
|
+
cmd: ["true"],
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
|
|
125
150
|
public writeSwJson(info: swJson.swJsonType, dstFile?: string) {
|
|
126
151
|
const epType = info.asset ? "asset" : "software";
|
|
127
152
|
const dstSwInfoPath =
|
|
@@ -297,6 +322,7 @@ export class SwJsonRenderer {
|
|
|
297
322
|
): swJson.remoteSoftwareType | undefined {
|
|
298
323
|
switch (mode) {
|
|
299
324
|
case "release":
|
|
325
|
+
case "dev-remote":
|
|
300
326
|
break;
|
|
301
327
|
|
|
302
328
|
case "dev-local":
|
|
@@ -433,6 +459,7 @@ export class SwJsonRenderer {
|
|
|
433
459
|
): swJson.runEnvInfo | undefined {
|
|
434
460
|
switch (mode) {
|
|
435
461
|
case "release":
|
|
462
|
+
case "dev-remote":
|
|
436
463
|
break;
|
|
437
464
|
|
|
438
465
|
case "dev-local":
|
|
@@ -482,6 +509,7 @@ export class SwJsonRenderer {
|
|
|
482
509
|
): swJson.assetInfo | undefined {
|
|
483
510
|
switch (mode) {
|
|
484
511
|
case "release":
|
|
512
|
+
case "dev-remote":
|
|
485
513
|
break;
|
|
486
514
|
|
|
487
515
|
case "dev-local":
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { hashDirSync, shouldDoAction } from "./util";
|
|
2
|
+
import { test, expect } from "vitest";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
const digest = (root: string): string => hashDirSync(root).digest("hex");
|
|
8
|
+
|
|
9
|
+
// Nested entries must hash under their full path from the root, otherwise distinct trees collide
|
|
10
|
+
// and content-addressable dev naming would serve a stale artifact.
|
|
11
|
+
test("hashDirSync distinguishes a nested file from a same-named root file", () => {
|
|
12
|
+
const a = fs.mkdtempSync(path.join(os.tmpdir(), "hash-a-"));
|
|
13
|
+
const b = fs.mkdtempSync(path.join(os.tmpdir(), "hash-b-"));
|
|
14
|
+
try {
|
|
15
|
+
// Tree A: an empty `dir/` plus a root `file`.
|
|
16
|
+
fs.mkdirSync(path.join(a, "dir"));
|
|
17
|
+
fs.writeFileSync(path.join(a, "file"), "x");
|
|
18
|
+
|
|
19
|
+
// Tree B: the same `file` (same contents) but nested inside `dir/`.
|
|
20
|
+
fs.mkdirSync(path.join(b, "dir"));
|
|
21
|
+
fs.writeFileSync(path.join(b, "dir", "file"), "x");
|
|
22
|
+
|
|
23
|
+
expect(digest(a)).not.toEqual(digest(b));
|
|
24
|
+
} finally {
|
|
25
|
+
fs.rmSync(a, { recursive: true, force: true });
|
|
26
|
+
fs.rmSync(b, { recursive: true, force: true });
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("hashDirSync is identical for identical trees at different locations", () => {
|
|
31
|
+
const a = fs.mkdtempSync(path.join(os.tmpdir(), "hash-a-"));
|
|
32
|
+
const b = fs.mkdtempSync(path.join(os.tmpdir(), "hash-b-"));
|
|
33
|
+
try {
|
|
34
|
+
for (const root of [a, b]) {
|
|
35
|
+
fs.mkdirSync(path.join(root, "dir"));
|
|
36
|
+
fs.writeFileSync(path.join(root, "dir", "nested"), "hello");
|
|
37
|
+
fs.writeFileSync(path.join(root, "top"), "world");
|
|
38
|
+
}
|
|
39
|
+
expect(digest(a)).toEqual(digest(b));
|
|
40
|
+
} finally {
|
|
41
|
+
fs.rmSync(a, { recursive: true, force: true });
|
|
42
|
+
fs.rmSync(b, { recursive: true, force: true });
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("shouldDoAction: disable wins over enable wins over the default", () => {
|
|
47
|
+
expect(shouldDoAction({ default: true })).toBe(true);
|
|
48
|
+
expect(shouldDoAction({ default: false })).toBe(false);
|
|
49
|
+
expect(shouldDoAction({ default: false, enable: true })).toBe(true);
|
|
50
|
+
expect(shouldDoAction({ default: true, disable: true })).toBe(false);
|
|
51
|
+
expect(shouldDoAction({ default: false, enable: true, disable: true })).toBe(false);
|
|
52
|
+
});
|
package/src/core/util.ts
CHANGED
|
@@ -10,7 +10,7 @@ import * as envs from "./envs";
|
|
|
10
10
|
export const packageJsonName = "package.json";
|
|
11
11
|
export const softwareConfigName = "package.json";
|
|
12
12
|
|
|
13
|
-
export function assertNever(_a: never) {
|
|
13
|
+
export function assertNever(_a: never): never {
|
|
14
14
|
throw new Error(`code logic error: assertNever() call for ${JSON.stringify(_a)}`);
|
|
15
15
|
}
|
|
16
16
|
|
|
@@ -51,7 +51,10 @@ export function hashDirSync(rootDir: string, hasher?: Hash, subdir?: string): Ha
|
|
|
51
51
|
const hash = hasher ? hasher : createHash("sha256");
|
|
52
52
|
const folder = path.join(rootDir, subdir ?? ".");
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
// Sort for a host-independent hash: readdir enumeration order is not guaranteed.
|
|
55
|
+
const info = fs
|
|
56
|
+
.readdirSync(folder, { withFileTypes: true })
|
|
57
|
+
.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
55
58
|
|
|
56
59
|
for (const item of info) {
|
|
57
60
|
const relPath = path.join(subdir ?? ".", item.name);
|
|
@@ -70,7 +73,9 @@ export function hashDirSync(rootDir: string, hasher?: Hash, subdir?: string): Ha
|
|
|
70
73
|
|
|
71
74
|
fs.closeSync(fileDescriptor);
|
|
72
75
|
} else if (item.isDirectory()) {
|
|
73
|
-
|
|
76
|
+
// Keep rootDir fixed and pass the accumulated relPath so nested entries hash under their
|
|
77
|
+
// full path from the root — otherwise `dirA/` + `file` collides with `dirA/file`.
|
|
78
|
+
hashDirSync(rootDir, hash, relPath);
|
|
74
79
|
}
|
|
75
80
|
}
|
|
76
81
|
|
|
@@ -295,11 +300,26 @@ export function currentPlatform(): PlatformType {
|
|
|
295
300
|
export const AllSoftwareSources = ["archive", "docker"] as const; // add 'image', '<whatever>' here when supported
|
|
296
301
|
export type SoftwareSource = (typeof AllSoftwareSources)[number];
|
|
297
302
|
|
|
298
|
-
export type BuildMode = "dev-local" | "release";
|
|
303
|
+
export type BuildMode = "dev-local" | "dev-remote" | "release";
|
|
304
|
+
|
|
305
|
+
export function isDevMode(mode: BuildMode): mode is "dev-local" | "dev-remote" {
|
|
306
|
+
return mode === "dev-local" || mode === "dev-remote";
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function producesRegistryDescriptor(mode: BuildMode): mode is "dev-remote" | "release" {
|
|
310
|
+
return mode === "dev-remote" || mode === "release";
|
|
311
|
+
}
|
|
299
312
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
313
|
+
// Resolve a `--do`/`--no-do` (or PL_*_/PL_*_NO_ env) tri-state: explicit disable wins, then enable,
|
|
314
|
+
// else the default.
|
|
315
|
+
export function shouldDoAction(opts: {
|
|
316
|
+
default: boolean;
|
|
317
|
+
enable?: boolean;
|
|
318
|
+
disable?: boolean;
|
|
319
|
+
}): boolean {
|
|
320
|
+
if (opts.disable) return false;
|
|
321
|
+
if (opts.enable) return true;
|
|
322
|
+
return opts.default;
|
|
303
323
|
}
|
|
304
324
|
|
|
305
325
|
export type artifactID = {
|
package/src/defaults.ts
CHANGED
|
@@ -8,6 +8,14 @@
|
|
|
8
8
|
export const BIN_REGISTRY_NAME = "platforma-open";
|
|
9
9
|
export const BIN_REGISTRY_DOWNLOAD_URL = "https://bin.pl-open.science/";
|
|
10
10
|
|
|
11
|
+
// Built-in dev binary registry name; flips to DEV_BIN_REGISTRY_NAME_OVERRIDDEN under PL_DEV_BINARY_UPLOAD_URL.
|
|
12
|
+
export const DEV_BIN_REGISTRY_NAME = "midev";
|
|
13
|
+
export const DEV_BIN_REGISTRY_NAME_OVERRIDDEN = "dev";
|
|
14
|
+
|
|
15
|
+
// Built-in midev dev binary upload endpoint (zero-config dev push), the binary counterpart of
|
|
16
|
+
// DEV_DOCKER_PUSH_TARGET. Override with PL_DEV_BINARY_UPLOAD_URL.
|
|
17
|
+
export const DEV_BINARY_UPLOAD_TARGET = "s3://milab-midev-registry?region=eu-central-1";
|
|
18
|
+
|
|
11
19
|
// Python
|
|
12
20
|
export const PYTHON_TOOLSET = "pip";
|
|
13
21
|
export const PYTHON_REQUIREMENTS_FILE = "requirements.txt";
|
|
@@ -27,9 +35,19 @@ export const DOCKER_REGISTRY = "containers.pl-open.science/milaboratories/pl-con
|
|
|
27
35
|
export const DOCKER_DOCKERFILE = "Dockerfile"; // inside git repo: path to Dockerfile to build by default
|
|
28
36
|
export const DOCKER_PLACEHOLDER_PKG = "/app"; // {pkg} placeholder value to by used in 'cmd' (for autogenerated docker images)
|
|
29
37
|
|
|
38
|
+
// Sentinel registry/package name for placeholder descriptors, so an accidental execution fails
|
|
39
|
+
// with a recognizable error instead of an opaque registry 404.
|
|
40
|
+
export const NO_SOFTWARE_PLACEHOLDER = "no-software-placeholder";
|
|
41
|
+
|
|
30
42
|
// Block-software dev pushes (pl-pkg build --dev=local) target this ECR by default.
|
|
31
43
|
// Override per-run with --docker-registry / PL_DOCKER_REGISTRY.
|
|
32
|
-
export const DEV_DOCKER_REGISTRY = "public.ecr.aws/
|
|
44
|
+
export const DEV_DOCKER_REGISTRY = "public.ecr.aws/miresearch/pl-containers";
|
|
45
|
+
|
|
46
|
+
// ecr:// scheme requests automatic docker login; the pull address stays the bare registry.
|
|
47
|
+
export const DEV_DOCKER_PUSH_TARGET = `ecr://${DEV_DOCKER_REGISTRY}`;
|
|
48
|
+
|
|
49
|
+
// Fallback AWS SSO profile for dev push; override with PL_AWS_PROFILE or AWS_PROFILE.
|
|
50
|
+
export const AWS_DEV_PROFILE = "research-poweruser";
|
|
33
51
|
|
|
34
52
|
// All docker images are built for and pushed as linux/amd64, regardless of host arch.
|
|
35
53
|
// Platforma's K8s nodes are linux/amd64; building for any other platform produces
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,9 @@ export * as util from "./core/util";
|
|
|
5
5
|
export * as envs from "./core/envs";
|
|
6
6
|
export * as defaults from "./defaults";
|
|
7
7
|
|
|
8
|
+
/** The logger the engine and its callers pass around. */
|
|
9
|
+
export type Logger = winston.Logger;
|
|
10
|
+
|
|
8
11
|
/**
|
|
9
12
|
* Public surface of the build engine. Derived from `Core` so signatures never
|
|
10
13
|
* drift, while the `Core` class itself stays internal (per A-0023: the engine is
|
|
@@ -22,6 +25,7 @@ export type Builder = Pick<
|
|
|
22
25
|
| "buildDockerImages"
|
|
23
26
|
| "buildSoftwareArchives"
|
|
24
27
|
| "buildSwJsonFiles"
|
|
28
|
+
| "writePublishedArtifactInfo"
|
|
25
29
|
| "publishPackages"
|
|
26
30
|
| "publishDockerImages"
|
|
27
31
|
>;
|