@tsplc/build 0.2.8 → 0.2.10

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/index.js CHANGED
@@ -53,7 +53,7 @@ var readBuild = async (directory) => {
53
53
  // packages/build/src/docker-build.ts
54
54
  import { execFile } from "node:child_process";
55
55
  import { randomUUID } from "node:crypto";
56
- import { cp as cp3, mkdir as mkdir3, mkdtemp as mkdtemp2, readFile as readFile3, rm as rm3 } from "node:fs/promises";
56
+ import { cp as cp4, mkdir as mkdir3, mkdtemp as mkdtemp2, readFile as readFile3, rm as rm3 } from "node:fs/promises";
57
57
  import { tmpdir as tmpdir2 } from "node:os";
58
58
  import { resolve as resolve5 } from "node:path";
59
59
  import { promisify } from "node:util";
@@ -134,7 +134,7 @@ var buildFailureSchema = z2.strictObject({
134
134
  // packages/build/src/docker-run.ts
135
135
  import { spawn } from "node:child_process";
136
136
  import { createHash } from "node:crypto";
137
- import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile3 } from "node:fs/promises";
137
+ import { copyFile as copyFile2, cp as cp3, mkdir as mkdir2, readFile as readFile2, writeFile as writeFile3 } from "node:fs/promises";
138
138
  import { basename, posix, relative, resolve as resolve4, sep } from "node:path";
139
139
  var runDocker = (args, failure) => new Promise((accept, reject) => {
140
140
  const child = spawn("docker", args, {
@@ -164,7 +164,7 @@ var runDocker = (args, failure) => new Promise((accept, reject) => {
164
164
  });
165
165
  });
166
166
  var bindMount = (source, target, readonly = false) => `type=bind,"source=${source.replaceAll('"', '""')}",target=${target}${readonly ? ",readonly" : ""}`;
167
- async function mountLibraries(libraries, directory) {
167
+ async function mountLibraries(libraries, directory, root = "/input") {
168
168
  if (!libraries?.length)
169
169
  return;
170
170
  await mkdir2(resolve4(directory, "libraries"), { recursive: true });
@@ -174,22 +174,30 @@ async function mountLibraries(libraries, directory) {
174
174
  throw new Error(`Library changed after installation: ${library.title}`);
175
175
  const filename = `${library.sha256}-${basename(library.path)}`;
176
176
  await writeFile3(resolve4(directory, "libraries", filename), data);
177
- return { ...library, path: `/input/libraries/${filename}` };
177
+ return { ...library, path: `${root}/libraries/${filename}` };
178
178
  }));
179
179
  }
180
+ var containerConfig = (config, paths, libraries) => ({
181
+ ...config,
182
+ libraries,
183
+ configFile: undefined,
184
+ docker: undefined,
185
+ codesys: undefined,
186
+ sourceDir: paths.sourceDir,
187
+ entry: posix.resolve(paths.sourceDir, relative(config.sourceDir, config.entry).split(sep).join("/")),
188
+ template: paths.template,
189
+ outDir: "/output",
190
+ runtime: undefined
191
+ });
192
+ var writeOperationRequest = async (config, directory, root) => {
193
+ await mkdir2(directory, { recursive: true });
194
+ await cp3(config.sourceDir, resolve4(directory, "sources"), { recursive: true });
195
+ await copyFile2(config.template, resolve4(directory, "template.project"));
196
+ await writeFile3(resolve4(directory, "config.json"), JSON.stringify(containerConfig(config, { sourceDir: `${root}/sources`, template: `${root}/template.project` }, await mountLibraries(config.libraries, directory, root))));
197
+ return `${root}/config.json`;
198
+ };
180
199
  var prepareContainerRun = async (config, requestDirectory, options) => {
181
- await writeFile3(resolve4(requestDirectory, "config.json"), JSON.stringify({
182
- ...config,
183
- libraries: options.sources ? await mountLibraries(config.libraries, requestDirectory) : [],
184
- configFile: undefined,
185
- docker: undefined,
186
- codesys: undefined,
187
- sourceDir: "/sources",
188
- entry: posix.resolve("/sources", relative(config.sourceDir, config.entry).split(sep).join("/")),
189
- template: "/template.project",
190
- outDir: "/output",
191
- runtime: undefined
192
- }));
200
+ await writeFile3(resolve4(requestDirectory, "config.json"), JSON.stringify(containerConfig(config, { sourceDir: "/sources", template: "/template.project" }, options.sources ? await mountLibraries(config.libraries, requestDirectory) : [])));
193
201
  return [
194
202
  "--init",
195
203
  ...config.docker?.cpuSet ? ["--cpuset-cpus", config.docker.cpuSet] : [],
@@ -246,7 +254,7 @@ var configureDocker = async () => {
246
254
  const cache = resolve5(context, "winetricks");
247
255
  await mkdir3(cache);
248
256
  if (process.env["CODESYS_WINETRICKS_CACHE"]) {
249
- await cp3(process.env["CODESYS_WINETRICKS_CACHE"], cache, {
257
+ await cp4(process.env["CODESYS_WINETRICKS_CACHE"], cache, {
250
258
  recursive: true
251
259
  });
252
260
  }
@@ -345,7 +353,7 @@ ${failure.message}`;
345
353
  // packages/build/src/host/codesys-worker.ts
346
354
  import {
347
355
  access,
348
- copyFile as copyFile2,
356
+ copyFile as copyFile3,
349
357
  mkdir as mkdir4,
350
358
  open,
351
359
  readFile as readFile4,
@@ -1254,7 +1262,7 @@ import { existsSync } from "node:fs";
1254
1262
  import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
1255
1263
  import { mkdir as mkdir5, mkdtemp as mkdtemp3, readFile as readFile6, rm as rm5 } from "node:fs/promises";
1256
1264
  import { tmpdir as tmpdir3 } from "node:os";
1257
- import { resolve as resolve8 } from "node:path";
1265
+ import { posix as posix2, resolve as resolve8 } from "node:path";
1258
1266
  import { setTimeout as delay2 } from "node:timers/promises";
1259
1267
  import { promisify as promisify4 } from "node:util";
1260
1268
  import { z as z5 } from "zod";
@@ -1287,6 +1295,22 @@ var controllerResultSchema = z5.strictObject({
1287
1295
  state: z5.string().nonempty(),
1288
1296
  values: z5.record(z5.string(), z5.string())
1289
1297
  });
1298
+ var controllerCredentialsSchema = z5.strictObject({
1299
+ username: z5.string().nonempty(),
1300
+ password: z5.string().nonempty()
1301
+ });
1302
+ var warmOperationSchema = z5.strictObject({
1303
+ operation: z5.enum(["download", "online-change"]),
1304
+ operationId: z5.uuid(),
1305
+ request: z5.string().nonempty(),
1306
+ initializeUser: z5.boolean(),
1307
+ credentials: controllerCredentialsSchema.optional()
1308
+ });
1309
+ var failureReason = (error) => {
1310
+ const message = error instanceof Error ? error.message : String(error);
1311
+ return (message.trim().split(`
1312
+ `).at(-1) ?? message).replace(/^\w+: /, "");
1313
+ };
1290
1314
  var controllerTarget = (config, online) => ({
1291
1315
  network: online.network,
1292
1316
  gateway: online.gateway,
@@ -1296,28 +1320,37 @@ var controllerTarget = (config, online) => ({
1296
1320
  deviceTree: createHash2("sha256").update(JSON.stringify(config.deviceTree)).digest("hex")
1297
1321
  }
1298
1322
  });
1299
- var controllerConnection = async (config) => {
1300
- if (!config.docker || !config.online) {
1301
- throw new Error("Docker image and online gateway/device configuration are required for controller operations");
1302
- }
1303
- const environmentNames = config.online.credentials ? [config.online.credentials.usernameEnv, config.online.credentials.passwordEnv] : [];
1304
- for (const name of environmentNames) {
1323
+ var credentialEnvironment = (config) => {
1324
+ const names = config.online?.credentials ? [config.online.credentials.usernameEnv, config.online.credentials.passwordEnv] : [];
1325
+ for (const name of names) {
1305
1326
  if (process.env[name] === undefined) {
1306
1327
  throw new Error(`Controller credential environment variable ${name} is not set`);
1307
1328
  }
1308
1329
  }
1330
+ return names;
1331
+ };
1332
+ var controllerConnection = async (config) => {
1333
+ if (!config.docker || !config.online) {
1334
+ throw new Error("Docker image and online gateway/device configuration are required for controller operations");
1335
+ }
1309
1336
  const { stdout } = await execute4("docker", ["image", "inspect", "--format", "{{.Id}}", dockerImage], { timeout: config.timeouts.hostCommand }).catch((cause) => {
1310
1337
  throw new Error(`Cannot inspect Docker builder image ${dockerImage}. Install and start Docker, then run tsplc-build configure if the image is missing`, { cause });
1311
1338
  });
1312
1339
  return {
1313
- environmentNames,
1314
1340
  imageId: imageIdSchema.parse(stdout.trim()),
1315
1341
  templateHash: createHash2("sha256").update(await readFile6(config.template)).digest("hex"),
1316
1342
  target: controllerTarget(config, config.online)
1317
1343
  };
1318
1344
  };
1345
+ var checkReceipt = (result, expected, diagnostics) => {
1346
+ if (result.operationId !== expected.operationId || result.operation !== expected.operation || result.imageId !== expected.imageId || result.templateHash !== expected.templateHash || JSON.stringify(result.target) !== JSON.stringify(expected.target)) {
1347
+ throw new Error(`Controller receipt does not match this operation's image, template or target. Inspect ${diagnostics} for diagnostics`);
1348
+ }
1349
+ return result;
1350
+ };
1319
1351
  var controllerOperation = async (config, operation, options) => {
1320
- const { environmentNames, imageId, templateHash, target } = await controllerConnection(config);
1352
+ const environmentNames = credentialEnvironment(config);
1353
+ const { imageId, templateHash, target } = await controllerConnection(config);
1321
1354
  const operationId = randomUUID2();
1322
1355
  const requestDirectory = await mkdtemp3(resolve8(tmpdir3(), "codesys-controller-"));
1323
1356
  const diagnostics = resolve8(config.outDir, ".codesys/controller");
@@ -1353,10 +1386,7 @@ var controllerOperation = async (config, operation, options) => {
1353
1386
  const result = await readFile6(resolve8(diagnostics, "result.json"), "utf8").then((content) => controllerResultSchema.parse(JSON.parse(content))).catch((cause) => {
1354
1387
  throw new Error(`Docker did not produce a valid controller receipt. Inspect ${diagnostics} for diagnostics`, { cause });
1355
1388
  });
1356
- if (result.operationId !== operationId || result.operation !== operation || result.imageId !== imageId || result.templateHash !== templateHash || JSON.stringify(result.target) !== JSON.stringify(target)) {
1357
- throw new Error(`Controller receipt does not match this operation's image, template or target. Inspect ${diagnostics} for diagnostics`);
1358
- }
1359
- return result;
1389
+ return checkReceipt(result, { operationId, operation, imageId, templateHash, target }, diagnostics);
1360
1390
  } finally {
1361
1391
  await rm5(requestDirectory, { recursive: true, force: true });
1362
1392
  }
@@ -1391,36 +1421,54 @@ var loginReplySchema = z5.discriminatedUnion("ok", [
1391
1421
  z5.strictObject({ ok: z5.literal(false), error: z5.string() })
1392
1422
  ]);
1393
1423
  var loginPort = 4851;
1424
+ var warmPort = 4852;
1394
1425
  var docker = (args, timeout) => execute4("docker", args, { timeout }).then(({ stdout }) => stdout.trim());
1395
1426
  var isRunning = async (name, timeout) => await docker(["container", "ls", "--quiet", "--filter", `name=^${name}$`], timeout) !== "";
1396
- var attachLogin = async (name, timeout) => {
1397
- const published = await docker(["port", name, String(loginPort)], timeout);
1398
- const url = `http://${published.split(`
1399
- `)[0]}`;
1400
- const call = async (init) => {
1401
- const reply = loginReplySchema.parse(await (await fetch(url, init)).json());
1402
- if (!reply.ok)
1403
- throw new Error(reply.error);
1404
- return reply.result;
1405
- };
1406
- return {
1427
+ var callContainer = async (name, port, method, body) => {
1428
+ const child = Bun.spawn([
1429
+ "docker",
1430
+ "exec",
1431
+ ...body === undefined ? [] : ["--interactive"],
1407
1432
  name,
1408
- ready: () => fetch(url).then((response) => response.ok).catch(() => false),
1409
- send: async (command) => loginResultSchema.parse(await call({
1410
- method: "POST",
1411
- headers: { "Content-Type": "application/json" },
1412
- body: JSON.stringify(command)
1413
- })),
1414
- close: async () => {
1415
- await call({ method: "DELETE" });
1416
- await docker(["wait", name], timeout);
1417
- await docker(["rm", name], timeout);
1418
- }
1419
- };
1433
+ "curl",
1434
+ "--silent",
1435
+ "--show-error",
1436
+ "--request",
1437
+ method,
1438
+ ...body === undefined ? [] : ["--header", "Content-Type: application/json", "--data-binary", "@-"],
1439
+ `http://127.0.0.1:${port}/`
1440
+ ], {
1441
+ stdin: body === undefined ? "ignore" : new Blob([JSON.stringify(body)]),
1442
+ stdout: "pipe",
1443
+ stderr: "pipe"
1444
+ });
1445
+ const [output, errors, code] = await Promise.all([
1446
+ new Response(child.stdout).text(),
1447
+ new Response(child.stderr).text(),
1448
+ child.exited
1449
+ ]);
1450
+ if (code !== 0)
1451
+ throw new Error(errors.trim() || `docker exec failed (${code})`);
1452
+ const reply = loginReplySchema.parse(JSON.parse(output));
1453
+ if (!reply.ok)
1454
+ throw new Error(reply.error);
1455
+ return reply.result;
1420
1456
  };
1457
+ var answers = (name, port) => callContainer(name, port, "GET").then(() => true, () => false);
1458
+ var attachLogin = (name, timeout) => ({
1459
+ name,
1460
+ ready: () => answers(name, loginPort),
1461
+ send: async (command) => loginResultSchema.parse(await callContainer(name, loginPort, "POST", command)),
1462
+ close: async () => {
1463
+ await callContainer(name, loginPort, "DELETE");
1464
+ await docker(["wait", name], timeout);
1465
+ await docker(["rm", name], timeout);
1466
+ }
1467
+ });
1421
1468
  var findControllerLogin = async (name) => await isRunning(name, 15000) ? attachLogin(name, 15000) : undefined;
1422
1469
  var openControllerLogin = async (config, name) => {
1423
- const { environmentNames, imageId, target } = await controllerConnection(config);
1470
+ const environmentNames = credentialEnvironment(config);
1471
+ const { imageId, target } = await controllerConnection(config);
1424
1472
  const timeout = config.timeouts.hostCommand;
1425
1473
  await docker(["rm", "--force", name], timeout);
1426
1474
  const requestDirectory = resolve8(config.outDir, ".codesys/login-request");
@@ -1430,8 +1478,6 @@ var openControllerLogin = async (config, name) => {
1430
1478
  "--detach",
1431
1479
  "--name",
1432
1480
  name,
1433
- "--publish",
1434
- `127.0.0.1::${loginPort}`,
1435
1481
  ...await prepareContainerRun(config, requestDirectory, {
1436
1482
  network: target.network,
1437
1483
  environment: environmentNames,
@@ -1446,23 +1492,99 @@ var openControllerLogin = async (config, name) => {
1446
1492
  "--port",
1447
1493
  String(loginPort)
1448
1494
  ], timeout);
1495
+ const login = attachLogin(name, timeout);
1449
1496
  const deadline = Date.now() + config.timeouts.startup + config.timeouts.build;
1450
1497
  while (Date.now() < deadline) {
1451
- const login = await isRunning(name, timeout) ? await attachLogin(name, timeout).catch(() => {
1452
- return;
1453
- }) : undefined;
1454
- if (!login && !await isRunning(name, timeout)) {
1498
+ if (!await isRunning(name, timeout)) {
1455
1499
  const logs = await execute4("docker", ["logs", "--tail", "40", name], { timeout });
1456
1500
  throw new Error(`The controller login failed:
1457
1501
  ${logs.stdout}${logs.stderr}`);
1458
1502
  }
1459
- if (login && await login.ready())
1503
+ if (await login.ready())
1460
1504
  return login;
1461
1505
  await delay2(2000);
1462
1506
  }
1463
1507
  await docker(["rm", "--force", name], timeout);
1464
1508
  throw new Error("The controller login did not open in time");
1465
1509
  };
1510
+ var warmController = async (config, name, connection) => {
1511
+ const timeout = config.timeouts.hostCommand;
1512
+ const { imageId, target } = connection;
1513
+ const settings = createHash2("sha256").update(JSON.stringify({ imageId, target, timeouts: config.timeouts, docker: config.docker })).digest("hex");
1514
+ const label = `tsplc.warm=${settings}`;
1515
+ const running = async () => await docker(["container", "ls", "--quiet", "--filter", `name=^${name}$`, "--filter", `label=${label}`], timeout) !== "";
1516
+ if (!await running()) {
1517
+ await docker(["rm", "--force", name], timeout);
1518
+ const requestDirectory = resolve8(config.outDir, ".codesys/warm-request");
1519
+ await mkdir5(requestDirectory, { recursive: true });
1520
+ await docker([
1521
+ "run",
1522
+ "--detach",
1523
+ "--name",
1524
+ name,
1525
+ "--label",
1526
+ label,
1527
+ ...await prepareContainerRun(config, requestDirectory, {
1528
+ network: target.network,
1529
+ sources: false
1530
+ }),
1531
+ imageId,
1532
+ "warm",
1533
+ "--request",
1534
+ "/input/config.json",
1535
+ "--image-id",
1536
+ imageId,
1537
+ "--port",
1538
+ String(warmPort)
1539
+ ], timeout);
1540
+ }
1541
+ const deadline = Date.now() + config.timeouts.startup;
1542
+ while (Date.now() < deadline) {
1543
+ if (!await running()) {
1544
+ const logs = await execute4("docker", ["logs", "--tail", "40", name], { timeout });
1545
+ throw new Error(`CODESYS failed to start:
1546
+ ${logs.stdout}${logs.stderr}`);
1547
+ }
1548
+ if (await answers(name, warmPort))
1549
+ return;
1550
+ await delay2(2000);
1551
+ }
1552
+ await docker(["rm", "--force", name], timeout);
1553
+ throw new Error("CODESYS did not start in time");
1554
+ };
1555
+ var startWarmController = async (config, name) => {
1556
+ await warmController(config, name, await controllerConnection(config));
1557
+ };
1558
+ var updateWarmController = async (config, name, operation, options = {}) => {
1559
+ const connection = await controllerConnection(config);
1560
+ await warmController(config, name, connection);
1561
+ const operationId = randomUUID2();
1562
+ const diagnostics = resolve8(config.outDir, ".codesys/controller");
1563
+ const directory = resolve8(config.outDir, ".codesys/requests", operationId);
1564
+ try {
1565
+ const request = await writeOperationRequest(config, directory, posix2.join("/output/.codesys/requests", operationId));
1566
+ const result = await callContainer(name, warmPort, "POST", {
1567
+ operation,
1568
+ operationId,
1569
+ request,
1570
+ initializeUser: options.initializeUser ?? false,
1571
+ credentials: options.credentials
1572
+ }).catch((error) => {
1573
+ throw new Error(`${failureReason(error)}. Inspect ${diagnostics} for diagnostics`, {
1574
+ cause: error
1575
+ });
1576
+ });
1577
+ return checkReceipt(controllerResultSchema.parse(result), {
1578
+ operationId,
1579
+ operation,
1580
+ imageId: connection.imageId,
1581
+ templateHash: connection.templateHash,
1582
+ target: connection.target
1583
+ }, diagnostics);
1584
+ } finally {
1585
+ await rm5(directory, { recursive: true, force: true });
1586
+ }
1587
+ };
1466
1588
  // packages/build/src/run.ts
1467
1589
  import { execFile as execFile5 } from "node:child_process";
1468
1590
  import { mkdtemp as mkdtemp4, readFile as readFile7, rm as rm6, writeFile as writeFile5 } from "node:fs/promises";
@@ -1702,9 +1824,11 @@ export {
1702
1824
  readBuild,
1703
1825
  resolveConfig,
1704
1826
  runProject,
1827
+ startWarmController,
1705
1828
  updateController,
1829
+ updateWarmController,
1706
1830
  validateLibrary
1707
1831
  };
1708
1832
 
1709
- //# debugId=2FE06AE03D021AE864756E2164756E21
1833
+ //# debugId=E1299EFC223A3AA564756E2164756E21
1710
1834
  //# sourceMappingURL=index.js.map