@platforma-sdk/package-builder-lib 1.1.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/assets/conda-dockerfile.template +45 -0
- package/assets/python-dockerfile.template +22 -0
- package/dist/_virtual/_rolldown/runtime.cjs +33 -0
- package/dist/_virtual/_rolldown/runtime.js +13 -0
- package/dist/core/archive.cjs +52 -0
- package/dist/core/archive.cjs.map +1 -0
- package/dist/core/archive.js +47 -0
- package/dist/core/archive.js.map +1 -0
- package/dist/core/conda/builder.cjs +226 -0
- package/dist/core/conda/builder.cjs.map +1 -0
- package/dist/core/conda/builder.js +220 -0
- package/dist/core/conda/builder.js.map +1 -0
- package/dist/core/conda/validate-spec.cjs +76 -0
- package/dist/core/conda/validate-spec.cjs.map +1 -0
- package/dist/core/conda/validate-spec.js +73 -0
- package/dist/core/conda/validate-spec.js.map +1 -0
- package/dist/core/core.cjs +403 -0
- package/dist/core/core.cjs.map +1 -0
- package/dist/core/core.d.ts +82 -0
- package/dist/core/core.d.ts.map +1 -0
- package/dist/core/core.js +397 -0
- package/dist/core/core.js.map +1 -0
- package/dist/core/docker-conda.cjs +75 -0
- package/dist/core/docker-conda.cjs.map +1 -0
- package/dist/core/docker-conda.js +71 -0
- package/dist/core/docker-conda.js.map +1 -0
- package/dist/core/docker-python.cjs +147 -0
- package/dist/core/docker-python.cjs.map +1 -0
- package/dist/core/docker-python.js +144 -0
- package/dist/core/docker-python.js.map +1 -0
- package/dist/core/docker.cjs +223 -0
- package/dist/core/docker.cjs.map +1 -0
- package/dist/core/docker.js +207 -0
- package/dist/core/docker.js.map +1 -0
- package/dist/core/envs.cjs +58 -0
- package/dist/core/envs.cjs.map +1 -0
- package/dist/core/envs.d.ts +26 -0
- package/dist/core/envs.d.ts.map +1 -0
- package/dist/core/envs.js +51 -0
- package/dist/core/envs.js.map +1 -0
- package/dist/core/package-info.cjs +450 -0
- package/dist/core/package-info.cjs.map +1 -0
- package/dist/core/package-info.d.ts +57 -0
- package/dist/core/package-info.d.ts.map +1 -0
- package/dist/core/package-info.js +447 -0
- package/dist/core/package-info.js.map +1 -0
- package/dist/core/paths.cjs +15 -0
- package/dist/core/paths.cjs.map +1 -0
- package/dist/core/paths.js +14 -0
- package/dist/core/paths.js.map +1 -0
- package/dist/core/resolver.cjs +99 -0
- package/dist/core/resolver.cjs.map +1 -0
- package/dist/core/resolver.js +95 -0
- package/dist/core/resolver.js.map +1 -0
- package/dist/core/schemas/artifacts.cjs +136 -0
- package/dist/core/schemas/artifacts.cjs.map +1 -0
- package/dist/core/schemas/artifacts.d.ts +280 -0
- package/dist/core/schemas/artifacts.d.ts.map +1 -0
- package/dist/core/schemas/artifacts.js +122 -0
- package/dist/core/schemas/artifacts.js.map +1 -0
- package/dist/core/schemas/entrypoints.cjs +43 -0
- package/dist/core/schemas/entrypoints.cjs.map +1 -0
- package/dist/core/schemas/entrypoints.d.ts +32 -0
- package/dist/core/schemas/entrypoints.d.ts.map +1 -0
- package/dist/core/schemas/entrypoints.js +41 -0
- package/dist/core/schemas/entrypoints.js.map +1 -0
- package/dist/core/schemas/sw-json.cjs +111 -0
- package/dist/core/schemas/sw-json.cjs.map +1 -0
- package/dist/core/schemas/sw-json.js +110 -0
- package/dist/core/schemas/sw-json.js.map +1 -0
- package/dist/core/storage.cjs +86 -0
- package/dist/core/storage.cjs.map +1 -0
- package/dist/core/storage.js +83 -0
- package/dist/core/storage.js.map +1 -0
- package/dist/core/sw-json-render.cjs +327 -0
- package/dist/core/sw-json-render.cjs.map +1 -0
- package/dist/core/sw-json-render.js +322 -0
- package/dist/core/sw-json-render.js.map +1 -0
- package/dist/core/util.cjs +313 -0
- package/dist/core/util.cjs.map +1 -0
- package/dist/core/util.d.ts +66 -0
- package/dist/core/util.d.ts.map +1 -0
- package/dist/core/util.js +281 -0
- package/dist/core/util.js.map +1 -0
- package/dist/defaults.cjs +54 -0
- package/dist/defaults.cjs.map +1 -0
- package/dist/defaults.d.ts +21 -0
- package/dist/defaults.d.ts.map +1 -0
- package/dist/defaults.js +37 -0
- package/dist/defaults.js.map +1 -0
- package/dist/index.cjs +31 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/package.json +56 -0
- package/src/__test__/Dockerfile +1 -0
- package/src/__test__/docker.test.ts +67 -0
- package/src/__test__/package.json +39 -0
- package/src/__test__/python-example/main.py +1 -0
- package/src/__test__/python-example/requirements.txt +0 -0
- package/src/__test__/vite.config.mts +7 -0
- package/src/core/__tests__/docker-conda.test.ts +145 -0
- package/src/core/__tests__/docker-entrypoint-generation.test.ts +119 -0
- package/src/core/__tests__/docker-python.test.ts +374 -0
- package/src/core/__tests__/publish-docker-images.test.ts +309 -0
- package/src/core/archive.ts +102 -0
- package/src/core/conda/builder.test.ts +71 -0
- package/src/core/conda/builder.ts +384 -0
- package/src/core/conda/validate-spec.test.ts +213 -0
- package/src/core/conda/validate-spec.ts +107 -0
- package/src/core/core.ts +835 -0
- package/src/core/docker-conda.ts +149 -0
- package/src/core/docker-python.ts +283 -0
- package/src/core/docker.ts +300 -0
- package/src/core/envs.ts +31 -0
- package/src/core/package-info.test.ts +82 -0
- package/src/core/package-info.ts +843 -0
- package/src/core/paths.ts +16 -0
- package/src/core/resolver.ts +195 -0
- package/src/core/schemas/artifacts.test.ts +72 -0
- package/src/core/schemas/artifacts.ts +279 -0
- package/src/core/schemas/entrypoints.test.ts +171 -0
- package/src/core/schemas/entrypoints.ts +150 -0
- package/src/core/schemas/sw-json.ts +216 -0
- package/src/core/schemas/test-artifacts.ts +362 -0
- package/src/core/storage.ts +125 -0
- package/src/core/sw-json-render.test.ts +215 -0
- package/src/core/sw-json-render.ts +682 -0
- package/src/core/util.ts +390 -0
- package/src/defaults.ts +38 -0
- package/src/index.ts +31 -0
- package/src/requirements.txt +1 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import * as os from "node:os";
|
|
5
|
+
import * as fsp from "node:fs/promises";
|
|
6
|
+
import { validateCondaSpec } from "./validate-spec";
|
|
7
|
+
import * as util from "../util";
|
|
8
|
+
|
|
9
|
+
describe("validateCondaSpec", () => {
|
|
10
|
+
let tempDir: string;
|
|
11
|
+
const mockLogger = util.createLogger();
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "validate-conda-spec-test"));
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const createSpec = async (content: string): Promise<string> => {
|
|
22
|
+
const specPath = path.join(tempDir, "spec.yaml");
|
|
23
|
+
await fsp.writeFile(specPath, content);
|
|
24
|
+
return specPath;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const expectValid = async (content: string) => {
|
|
28
|
+
const specPath = await createSpec(content);
|
|
29
|
+
expect(() => validateCondaSpec(mockLogger, specPath)).not.toThrow();
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const expectInvalid = async (content: string, errorPattern: RegExp) => {
|
|
33
|
+
const specPath = await createSpec(content);
|
|
34
|
+
expect(() => validateCondaSpec(mockLogger, specPath)).toThrow(errorPattern);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
describe("allowed channels", () => {
|
|
38
|
+
it("should accept single allowed channel", async () => {
|
|
39
|
+
await expectValid(`
|
|
40
|
+
channels:
|
|
41
|
+
- bioconda
|
|
42
|
+
dependencies:
|
|
43
|
+
- anarci
|
|
44
|
+
`);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("should accept multiple allowed channels", async () => {
|
|
48
|
+
await expectValid(`
|
|
49
|
+
channels:
|
|
50
|
+
- bioconda
|
|
51
|
+
- conda-forge
|
|
52
|
+
dependencies: []
|
|
53
|
+
`);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("forbidden channels", () => {
|
|
58
|
+
const forbiddenChannels = [
|
|
59
|
+
{ name: "main", variations: ["main", "MAIN"] },
|
|
60
|
+
{ name: "r", variations: ["r", "R"] },
|
|
61
|
+
{ name: "msys2", variations: ["msys2", "Msys2"] },
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
forbiddenChannels.forEach(({ name, variations }) => {
|
|
65
|
+
variations.forEach((variation) => {
|
|
66
|
+
it(`should reject ${name} channel (${variation})`, async () => {
|
|
67
|
+
await expectInvalid(
|
|
68
|
+
`
|
|
69
|
+
channels:
|
|
70
|
+
- conda-forge
|
|
71
|
+
- ${variation}
|
|
72
|
+
dependencies: []
|
|
73
|
+
`,
|
|
74
|
+
new RegExp(`Forbidden channel '${variation}'`),
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("allowed dependencies", () => {
|
|
82
|
+
it("should accept simple package names", async () => {
|
|
83
|
+
await expectValid(`
|
|
84
|
+
channels:
|
|
85
|
+
- conda-forge
|
|
86
|
+
dependencies:
|
|
87
|
+
- python
|
|
88
|
+
- numpy
|
|
89
|
+
`);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("should accept packages with allowed channel prefix", async () => {
|
|
93
|
+
await expectValid(`
|
|
94
|
+
channels:
|
|
95
|
+
- bioconda
|
|
96
|
+
dependencies:
|
|
97
|
+
- bioconda::anarci
|
|
98
|
+
- conda-forge::python
|
|
99
|
+
`);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("forbidden dependencies prefixes (case insensitive)", () => {
|
|
104
|
+
const forbiddenPrefixes = [
|
|
105
|
+
{ prefix: "main", package: "python" },
|
|
106
|
+
{ prefix: "MAIN", package: "stats" },
|
|
107
|
+
{ prefix: "r", package: "some-package" },
|
|
108
|
+
{ prefix: "R", package: "some-package" },
|
|
109
|
+
{ prefix: "msys2", package: "git" },
|
|
110
|
+
{ prefix: "Msys2", package: "git" },
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
forbiddenPrefixes.forEach(({ prefix, package: pkg }) => {
|
|
114
|
+
it(`should reject ${prefix}::${pkg}`, async () => {
|
|
115
|
+
await expectInvalid(
|
|
116
|
+
`
|
|
117
|
+
channels:
|
|
118
|
+
- conda-forge
|
|
119
|
+
dependencies:
|
|
120
|
+
- ${prefix}::${pkg}
|
|
121
|
+
`,
|
|
122
|
+
new RegExp(`Forbidden channel prefix '${prefix}::'`),
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("mixed scenarios", () => {
|
|
129
|
+
it("should accept allowed channels with allowed dependencies", async () => {
|
|
130
|
+
await expectValid(`
|
|
131
|
+
channels:
|
|
132
|
+
- bioconda
|
|
133
|
+
- conda-forge
|
|
134
|
+
dependencies:
|
|
135
|
+
- anarci
|
|
136
|
+
- bioconda::biotools
|
|
137
|
+
- python
|
|
138
|
+
`);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("should reject when both channels and dependencies have violations", async () => {
|
|
142
|
+
const content = `
|
|
143
|
+
channels:
|
|
144
|
+
- main
|
|
145
|
+
- conda-forge
|
|
146
|
+
dependencies:
|
|
147
|
+
- main::python
|
|
148
|
+
- anarci
|
|
149
|
+
`;
|
|
150
|
+
const specPath = await createSpec(content);
|
|
151
|
+
|
|
152
|
+
expect(() => validateCondaSpec(mockLogger, specPath)).toThrow(
|
|
153
|
+
/Forbidden channel 'main'[\s\S]*Forbidden channel prefix 'main::'/,
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("should reject multiple violations", async () => {
|
|
158
|
+
await expectInvalid(
|
|
159
|
+
`
|
|
160
|
+
channels:
|
|
161
|
+
- main
|
|
162
|
+
- r
|
|
163
|
+
- msys2
|
|
164
|
+
dependencies:
|
|
165
|
+
- main::python
|
|
166
|
+
- r::stats
|
|
167
|
+
- msys2::git
|
|
168
|
+
`,
|
|
169
|
+
/.+/,
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe("edge cases", () => {
|
|
175
|
+
it("should handle empty channels list", async () => {
|
|
176
|
+
await expectValid(`
|
|
177
|
+
channels: []
|
|
178
|
+
dependencies:
|
|
179
|
+
- python
|
|
180
|
+
`);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("should handle empty dependencies list", async () => {
|
|
184
|
+
await expectValid(`
|
|
185
|
+
channels:
|
|
186
|
+
- conda-forge
|
|
187
|
+
dependencies: []
|
|
188
|
+
`);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("should handle specs without dependencies section", async () => {
|
|
192
|
+
await expectValid(`
|
|
193
|
+
channels:
|
|
194
|
+
- conda-forge
|
|
195
|
+
`);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("should throw error for non-existent file", () => {
|
|
199
|
+
expect(() => validateCondaSpec(mockLogger, "/non-existent/path.yaml")).toThrow();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("should handle dependencies with version constraints", async () => {
|
|
203
|
+
await expectValid(`
|
|
204
|
+
channels:
|
|
205
|
+
- conda-forge
|
|
206
|
+
dependencies:
|
|
207
|
+
- python=3.9
|
|
208
|
+
- numpy>=1.20.0
|
|
209
|
+
- matplotlib<3.5
|
|
210
|
+
`);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import yaml from "yaml";
|
|
3
|
+
import type winston from "winston";
|
|
4
|
+
|
|
5
|
+
export interface CondaSpec {
|
|
6
|
+
channels?: string[];
|
|
7
|
+
dependencies?: (string | Record<string, unknown>)[];
|
|
8
|
+
name?: string;
|
|
9
|
+
prefix?: string;
|
|
10
|
+
[key: string]: unknown;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Channels that are forbidden because they reference the official Anaconda channel.
|
|
15
|
+
* These channels (main, r, msys2, defaults, anaconda) should be avoided in favor of
|
|
16
|
+
* community-maintained channels like conda-forge to avoid licensing restrictions
|
|
17
|
+
*/
|
|
18
|
+
const FORBIDDEN_CHANNELS = ["main", "r", "msys2", "defaults", "anaconda"];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Validates a conda spec file to ensure it doesn't use forbidden channels
|
|
22
|
+
* @param logger Winston logger instance
|
|
23
|
+
* @param specPath Path to the conda spec file
|
|
24
|
+
* @throws Error if forbidden channels are found
|
|
25
|
+
*/
|
|
26
|
+
export function validateCondaSpec(logger: winston.Logger, specPath: string): void {
|
|
27
|
+
logger.debug(`Validating conda spec file: ${specPath}`);
|
|
28
|
+
|
|
29
|
+
if (!fs.existsSync(specPath)) {
|
|
30
|
+
throw new Error(`Conda spec file not found: ${specPath}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const specContent = fs.readFileSync(specPath, "utf-8");
|
|
34
|
+
const spec = yaml.parse(specContent) as CondaSpec;
|
|
35
|
+
|
|
36
|
+
if (!spec || typeof spec !== "object") {
|
|
37
|
+
throw new Error(`Invalid conda spec file format: ${specPath}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const errors: string[] = [];
|
|
41
|
+
|
|
42
|
+
if (spec.channels) {
|
|
43
|
+
const channelErrors = validateChannels(spec.channels);
|
|
44
|
+
if (channelErrors.length > 0) {
|
|
45
|
+
errors.push(...channelErrors);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (spec.dependencies) {
|
|
50
|
+
const depErrors = validateDependencies(spec.dependencies);
|
|
51
|
+
if (depErrors.length > 0) {
|
|
52
|
+
errors.push(...depErrors);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (errors.length > 0) {
|
|
57
|
+
throw new Error(`Conda spec file contains forbidden Anaconda channels:\n${errors.join("\n")}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
logger.debug(`Conda spec file validation passed: ${specPath}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Validates that no forbidden channels are in the channels list
|
|
65
|
+
* @param channels Array of channel names
|
|
66
|
+
* @returns Array of error messages for forbidden channels
|
|
67
|
+
*/
|
|
68
|
+
function validateChannels(channels: string[]): string[] {
|
|
69
|
+
const errors: string[] = [];
|
|
70
|
+
|
|
71
|
+
for (const channel of channels) {
|
|
72
|
+
const channelLower = channel.toLowerCase().trim();
|
|
73
|
+
if (FORBIDDEN_CHANNELS.includes(channelLower)) {
|
|
74
|
+
errors.push(` - Forbidden channel '${channel}' in channels list`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return errors;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Validates that no dependencies use forbidden channel prefixes
|
|
83
|
+
* @param dependencies Array of dependencies (can be strings or objects)
|
|
84
|
+
* @returns Array of error messages for forbidden dependencies
|
|
85
|
+
*/
|
|
86
|
+
function validateDependencies(dependencies: (string | Record<string, unknown>)[]): string[] {
|
|
87
|
+
const errors: string[] = [];
|
|
88
|
+
|
|
89
|
+
for (const dep of dependencies) {
|
|
90
|
+
// Skip non-string dependencies (they might be pip dependencies, etc.)
|
|
91
|
+
if (typeof dep !== "string") {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Check if dependency uses channel prefix (format: channel::package)
|
|
96
|
+
if (dep.includes("::")) {
|
|
97
|
+
const [channel, ..._packageParts] = dep.split("::");
|
|
98
|
+
const channelLower = channel.toLowerCase().trim();
|
|
99
|
+
|
|
100
|
+
if (FORBIDDEN_CHANNELS.includes(channelLower)) {
|
|
101
|
+
errors.push(` - Forbidden channel prefix '${channel}::' in dependency '${dep}'`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return errors;
|
|
107
|
+
}
|