@agoric/xsnap 0.14.3-dev-fa6d3d3.0.fa6d3d3 → 0.14.3-dev-e3099cb.0.e3099cb

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agoric/xsnap",
3
- "version": "0.14.3-dev-fa6d3d3.0.fa6d3d3",
3
+ "version": "0.14.3-dev-e3099cb.0.e3099cb",
4
4
  "description": "Snapshotting VM worker based on Moddable's XS Javascript engine",
5
5
  "author": "Agoric",
6
6
  "license": "Apache-2.0",
@@ -13,7 +13,7 @@
13
13
  "scripts": {
14
14
  "repl": "node src/xsrepl.js",
15
15
  "build:bin": "/bin/sh -c 'if test -d ./test; then node src/build.js; else yarn build:from-env; fi'",
16
- "build:env": "node src/build.js --show-env > build.env",
16
+ "build:env": "/bin/sh -c 'node src/build.js --show-env > build.env.new && mv build.env.new build.env'",
17
17
  "build:from-env": "{ cat build.env; echo node src/build.js; } | xargs env",
18
18
  "build": "yarn build:bin && yarn build:env",
19
19
  "check-version": "/bin/sh -c 'if test \"${npm_package_version}\" != \"$(./scripts/get_xsnap_version.sh)\"; then echo \"xsnap version mismatch; expected '${npm_package_version}'\"; exit 1; fi'",
@@ -28,8 +28,8 @@
28
28
  "test:xs": "exit 0"
29
29
  },
30
30
  "dependencies": {
31
- "@agoric/internal": "0.3.3-dev-fa6d3d3.0.fa6d3d3",
32
- "@agoric/xsnap-lockdown": "0.14.1-dev-fa6d3d3.0.fa6d3d3",
31
+ "@agoric/internal": "0.3.3-dev-e3099cb.0.e3099cb",
32
+ "@agoric/xsnap-lockdown": "0.14.1-dev-e3099cb.0.e3099cb",
33
33
  "@endo/bundle-source": "^4.1.2",
34
34
  "@endo/errors": "^1.2.13",
35
35
  "@endo/eventual-send": "^1.3.4",
@@ -80,5 +80,5 @@
80
80
  "engines": {
81
81
  "node": "^20.9 || ^22.11"
82
82
  },
83
- "gitHead": "fa6d3d3633f3a11ab3946e0d02901883b71847a9"
83
+ "gitHead": "e3099cb9b5af514813cf606753b3743fd361bcf8"
84
84
  }
@@ -9,7 +9,7 @@ TEMP=$(mktemp -d)
9
9
  # }
10
10
  # trap cleanup EXIT
11
11
 
12
- yarn pack -f "$TEMP/package.tar"
12
+ yarn pack --out "$TEMP/package.tar"
13
13
  (
14
14
  cd "$TEMP"
15
15
  tar xvf package.tar
package/src/build.js CHANGED
@@ -31,7 +31,6 @@ const ModdableSDK = {
31
31
  platforms: {
32
32
  Linux: { path: 'lin' },
33
33
  Darwin: { path: 'mac' },
34
- Windows_NT: { path: 'win', make: 'nmake' },
35
34
  },
36
35
  buildGoals: ['release', 'debug'],
37
36
  };
@@ -101,130 +100,119 @@ function makeCLI(command, { spawn }) {
101
100
  });
102
101
  }
103
102
 
103
+ /** @param {string} repoUrl */
104
+ const canonicalRepoUrl = repoUrl =>
105
+ repoUrl.replace(/\/+$/, '').replace(/\.git$/, '');
106
+
104
107
  /**
105
- * @param {string} path
106
108
  * @param {string} repoUrl
107
- * @param {{ git: ReturnType<typeof makeCLI> }} io
109
+ * @param {string} commitHash
108
110
  */
109
- const makeSubmodule = (path, repoUrl, { git }) => {
110
- return freeze({
111
- path,
112
- clone: async () => git.run(['clone', repoUrl, path]),
113
- /** @param {string} commitHash */
114
- checkout: async commitHash =>
115
- git.run(['checkout', commitHash], { cwd: path }),
116
- init: async () => git.run(['submodule', 'update', '--init', '--checkout']),
117
- status: async () => {
118
- const line = await git.pipe(['submodule', 'status', path]);
119
- // From `git submodule --help`:
120
- // status [--cached] [--recursive] [--] [<path>...]
121
- // Show the status of the submodules. This will print the SHA-1 of the
122
- // currently checked out commit for each submodule, along with the
123
- // submodule path and the output of git describe for the SHA-1. Each
124
- // SHA-1 will possibly be prefixed with - if the submodule is not
125
- // initialized, + if the currently checked out submodule commit does
126
- // not match the SHA-1 found in the index of the containing repository
127
- // and U if the submodule has merge conflicts.
128
- //
129
- // We discovered that in other cases, the prefix is a single space.
130
- const prefix = line[0];
131
- const [hash, statusPath, ...describe] = line.slice(1).split(' ');
132
- return {
133
- prefix,
134
- hash,
135
- path: statusPath,
136
- describe: describe.join(' '),
137
- };
138
- },
139
- /**
140
- * Read a specific configuration value for this submodule (e.g., "path" or
141
- * "url") from the top-level .gitmodules.
142
- *
143
- * @param {string} leaf
144
- */
145
- config: async leaf => {
146
- // git rev-parse --show-toplevel
147
- const repoRoot = await git.pipe(['rev-parse', '--show-toplevel']);
148
- if (!path.startsWith(`${repoRoot}/`)) {
149
- throw Error(
150
- `Expected submodule path ${path} to be a subdirectory of repository ${repoRoot}`,
151
- );
152
- }
153
- const relativePath = path.slice(repoRoot.length + 1);
154
- // git config -f ../../.gitmodules --get submodule.${relativePath}.${leaf}
155
- const value = await git.pipe([
156
- 'config',
157
- '-f',
158
- `${repoRoot}/.gitmodules`,
159
- '--get',
160
- `submodule.${relativePath}.${leaf}`,
161
- ]);
162
- return value;
163
- },
164
- });
111
+ const defaultArchiveUrl = (repoUrl, commitHash) =>
112
+ `${canonicalRepoUrl(repoUrl)}/archive/${commitHash}.tar.gz`;
113
+
114
+ const SOURCE_STAMP_FILE = '.agoric-source-stamp.json';
115
+
116
+ /**
117
+ * @param {string} text
118
+ * @returns {Record<string, string>}
119
+ */
120
+ const parseEnvText = text => {
121
+ /** @type {Record<string, string>} */
122
+ const envMap = {};
123
+ for (const line of text.split('\n')) {
124
+ const trimmed = line.trim();
125
+ if (!trimmed || trimmed.startsWith('#')) continue;
126
+ const index = trimmed.indexOf('=');
127
+ if (index <= 0) continue;
128
+ const key = trimmed.slice(0, index);
129
+ const value = trimmed.slice(index + 1);
130
+ envMap[key] = value;
131
+ }
132
+ return envMap;
165
133
  };
166
134
 
167
135
  /**
168
136
  * @typedef {{
169
137
  * url: string,
170
138
  * path: string,
171
- * commitHash?: string,
139
+ * commitHash: string,
140
+ * archiveUrl: string,
172
141
  * envPrefix: string,
173
- * }} SubmoduleDescriptor
142
+ * }} SourceDescriptor
174
143
  */
175
144
 
176
145
  /**
177
- * @param {SubmoduleDescriptor[]} submodules
146
+ * @param {SourceDescriptor[]} sources
178
147
  * @param {{
179
- * git: ReturnType<typeof makeCLI>,
180
148
  * stdout: typeof process.stdout,
181
149
  * }} io
182
150
  */
183
- const showEnv = async (submodules, { git, stdout }) => {
151
+ const showEnv = async (sources, { stdout }) => {
184
152
  await null;
185
- for (const desc of submodules) {
186
- const { path, envPrefix } = desc;
187
- let { url, commitHash } = desc;
188
- if (!commitHash) {
189
- // We need to glean the commitHash and url from Git.
190
- const submodule = makeSubmodule(path, '?', { git });
191
- const [{ hash }, gitUrl] = await Promise.all([
192
- submodule.status(),
193
- submodule.config('url'),
194
- ]);
195
- commitHash = hash;
196
- url = gitUrl;
197
- }
153
+ for (const { envPrefix, url, commitHash, archiveUrl } of sources) {
198
154
  stdout.write(`${envPrefix}URL=${url}\n`);
199
155
  stdout.write(`${envPrefix}COMMIT_HASH=${commitHash}\n`);
156
+ const defaultUrl = defaultArchiveUrl(url, commitHash);
157
+ if (archiveUrl && archiveUrl !== defaultUrl) {
158
+ stdout.write(`${envPrefix}ARCHIVE_URL=${archiveUrl}\n`);
159
+ }
200
160
  }
201
161
  };
202
162
 
203
163
  /**
204
- * @param {SubmoduleDescriptor[]} submodules
164
+ * @param {SourceDescriptor[]} sources
205
165
  * @param {{
206
- * fs: Pick<typeof import('fs'), 'existsSync' | 'rmdirSync'>,
207
- * git: ReturnType<typeof makeCLI>,
166
+ * fs: Pick<typeof import('fs'), 'existsSync'> &
167
+ * Pick<typeof promises, 'mkdir' | 'rm' | 'readFile' | 'writeFile' | 'rename'>,
168
+ * curl: ReturnType<typeof makeCLI>,
169
+ * tar: ReturnType<typeof makeCLI>,
208
170
  * }} io
209
171
  */
210
- const updateSubmodules = async (submodules, { fs, git }) => {
172
+ const updateSources = async (sources, { fs, curl, tar }) => {
211
173
  await null;
212
- for (const { url, path, commitHash } of submodules) {
213
- const submodule = makeSubmodule(path, url, { git });
174
+ for (const { archiveUrl, path, commitHash, url } of sources) {
175
+ const stamp = JSON.stringify({ url, commitHash, archiveUrl }, null, 2);
176
+ const tmpPath = `${path}.tmp.${process.pid}.${Date.now()}`;
177
+ const oldPath = `${path}.old.${process.pid}.${Date.now()}`;
178
+ const archivePath = `${tmpPath}.tar.gz`;
179
+ const hadExistingPath = fs.existsSync(path);
214
180
 
215
- if (!commitHash) {
216
- await submodule.init();
217
- } else {
218
- // Do the moral equivalent of submodule update when explicitly overriding.
219
- try {
220
- fs.rmdirSync(submodule.path);
221
- } catch (_e) {
222
- // ignore
181
+ await fs.rm(tmpPath, { recursive: true, force: true });
182
+ await fs.rm(oldPath, { recursive: true, force: true });
183
+ await fs.mkdir(tmpPath, { recursive: true });
184
+
185
+ try {
186
+ await curl.run(['-fsSL', archiveUrl, '-o', archivePath]);
187
+ await tar.run([
188
+ '-xzf',
189
+ archivePath,
190
+ '--strip-components=1',
191
+ '-C',
192
+ tmpPath,
193
+ ]);
194
+ await fs.writeFile(`${tmpPath}/${SOURCE_STAMP_FILE}`, `${stamp}\n`);
195
+
196
+ if (hadExistingPath) {
197
+ await fs.rename(path, oldPath);
223
198
  }
224
- if (!fs.existsSync(submodule.path)) {
225
- await submodule.clone();
199
+
200
+ try {
201
+ await fs.rename(tmpPath, path);
202
+ } catch (err) {
203
+ if (hadExistingPath && fs.existsSync(oldPath)) {
204
+ await fs.rename(oldPath, path);
205
+ }
206
+ throw err;
226
207
  }
227
- await submodule.checkout(commitHash);
208
+ } catch (err) {
209
+ throw Error(
210
+ `Failed to fetch archive for ${path} @ ${commitHash} from ${archiveUrl}: ${err}`,
211
+ );
212
+ } finally {
213
+ await fs.rm(archivePath, { force: true });
214
+ await fs.rm(tmpPath, { recursive: true, force: true });
215
+ await fs.rm(oldPath, { recursive: true, force: true });
228
216
  }
229
217
  }
230
218
  };
@@ -282,8 +270,8 @@ const buildXsnap = async (platform, force, { fs, make }) => {
282
270
  * env: Record<string, string | undefined>,
283
271
  * stdout: typeof process.stdout,
284
272
  * spawn: typeof spawn,
285
- * fs: Pick<typeof import('fs'), 'existsSync' | 'rmdirSync'> &
286
- * Pick<typeof promises, 'readFile' | 'writeFile'>,
273
+ * fs: Pick<typeof import('fs'), 'existsSync'> &
274
+ * Pick<typeof promises, 'readFile' | 'writeFile' | 'mkdir' | 'rm' | 'rename'>,
287
275
  * os: Pick<typeof import('os'), 'type'>,
288
276
  * }} io
289
277
  */
@@ -298,60 +286,156 @@ async function main(args, { env, stdout, spawn, fs, os }) {
298
286
  throw Error(`xsnap does not support OS ${osType}`);
299
287
  }
300
288
 
301
- const git = makeCLI('git', { spawn });
289
+ const curl = makeCLI('curl', { spawn });
290
+ const tar = makeCLI('tar', { spawn });
302
291
  const make = makeCLI(platform.make || 'make', { spawn });
303
292
 
293
+ /** @type {Record<string, string>} */
294
+ let pinnedEnvFromFile = {};
295
+ let hasPinnedEnvFile = false;
296
+ try {
297
+ const text = await fs.readFile(asset('../build.env'), 'utf-8');
298
+ pinnedEnvFromFile = parseEnvText(text);
299
+ hasPinnedEnvFile = true;
300
+ } catch (_err) {
301
+ // Allow explicit environment overrides to run without a checked-in build.env.
302
+ }
303
+
304
+ const moddableUrl =
305
+ env.MODDABLE_URL ||
306
+ pinnedEnvFromFile.MODDABLE_URL ||
307
+ 'https://github.com/agoric-labs/moddable.git';
308
+ const moddableCommitHash =
309
+ env.MODDABLE_COMMIT_HASH || pinnedEnvFromFile.MODDABLE_COMMIT_HASH;
310
+ if (!moddableCommitHash) {
311
+ throw Error(
312
+ 'Missing MODDABLE_COMMIT_HASH; set it in env or packages/xsnap/build.env',
313
+ );
314
+ }
315
+
316
+ const xsnapNativeUrl =
317
+ env.XSNAP_NATIVE_URL ||
318
+ pinnedEnvFromFile.XSNAP_NATIVE_URL ||
319
+ 'https://github.com/agoric-labs/xsnap-pub';
320
+ const xsnapNativeCommitHash =
321
+ env.XSNAP_NATIVE_COMMIT_HASH || pinnedEnvFromFile.XSNAP_NATIVE_COMMIT_HASH;
322
+ if (!xsnapNativeCommitHash) {
323
+ throw Error(
324
+ 'Missing XSNAP_NATIVE_COMMIT_HASH; set it in env or packages/xsnap/build.env',
325
+ );
326
+ }
327
+
304
328
  // When changing/adding entries here, make sure to search the whole project
305
- // for `@@AGORIC_DOCKER_SUBMODULES@@`
306
- const submodules = [
329
+ // for `@@AGORIC_DOCKER_SUBMODULES@@` in container build wiring.
330
+ /** @type {SourceDescriptor[]} */
331
+ const sources = [
307
332
  {
308
- url: env.MODDABLE_URL || 'https://github.com/agoric-labs/moddable.git',
333
+ url: moddableUrl,
309
334
  path: ModdableSDK.MODDABLE,
310
- commitHash: env.MODDABLE_COMMIT_HASH,
335
+ commitHash: moddableCommitHash,
336
+ archiveUrl:
337
+ env.MODDABLE_ARCHIVE_URL ||
338
+ defaultArchiveUrl(moddableUrl, moddableCommitHash),
311
339
  envPrefix: 'MODDABLE_',
312
340
  },
313
341
  {
314
- url:
315
- env.XSNAP_NATIVE_URL || 'https://github.com/agoric-labs/xsnap-pub.git',
342
+ url: xsnapNativeUrl,
316
343
  path: asset('../xsnap-native'),
317
- commitHash: env.XSNAP_NATIVE_COMMIT_HASH,
344
+ commitHash: xsnapNativeCommitHash,
345
+ archiveUrl:
346
+ env.XSNAP_NATIVE_ARCHIVE_URL ||
347
+ defaultArchiveUrl(xsnapNativeUrl, xsnapNativeCommitHash),
318
348
  envPrefix: 'XSNAP_NATIVE_',
319
349
  },
320
350
  ];
321
351
 
322
352
  // We build both release and debug executables, so checking for only the
323
353
  // former is fine.
324
- // XXX This will need to account for the .exe extension if we recover support
325
- // for Windows.
326
354
  const bin = asset(
327
355
  `../xsnap-native/xsnap/build/bin/${platform.path}/release/xsnap-worker`,
328
356
  );
357
+ /** @type {Map<string, SourceDescriptor>} */
358
+ const sourceByPrefix = new Map(
359
+ sources.map(source => [source.envPrefix, source]),
360
+ );
361
+
362
+ /**
363
+ * @param {SourceDescriptor} source
364
+ * @returns {Promise<boolean>} whether the source needs to be refreshed from
365
+ * its archive URL based on whether the existing source stamp matches the
366
+ * expected URL and commit hash
367
+ * @throws if the source stamp exists but cannot be read or parsed
368
+ */
369
+ const needsSourceRefresh = async source => {
370
+ await null;
371
+ try {
372
+ const stampText = await fs.readFile(
373
+ `${source.path}/${SOURCE_STAMP_FILE}`,
374
+ 'utf-8',
375
+ );
376
+ const stamp = JSON.parse(stampText);
377
+ return (
378
+ stamp.url !== source.url ||
379
+ stamp.commitHash !== source.commitHash ||
380
+ stamp.archiveUrl !== source.archiveUrl
381
+ );
382
+ } catch {
383
+ return true;
384
+ }
385
+ };
386
+
329
387
  const hasBin = fs.existsSync(bin);
330
388
  const hasSource = fs.existsSync(asset('../moddable/xs/includes/xs.h'));
331
- const hasGit = fs.existsSync(asset('../moddable/.git'));
389
+ const hasRepoGit = fs.existsSync(asset('../../../.git'));
390
+ const hasExplicitOverride = prefix => {
391
+ const source = sourceByPrefix.get(prefix);
392
+ if (!source) return false;
393
+ const urlKey = `${prefix}URL`;
394
+ const hashKey = `${prefix}COMMIT_HASH`;
395
+ const archiveKey = `${prefix}ARCHIVE_URL`;
396
+ const fileUrl = pinnedEnvFromFile[urlKey];
397
+ const fileHash = pinnedEnvFromFile[hashKey];
398
+ const fileArchive =
399
+ pinnedEnvFromFile[archiveKey] ||
400
+ defaultArchiveUrl(source.url, source.commitHash);
401
+ const envUrl = env[urlKey];
402
+ const envHash = env[hashKey];
403
+ const envArchive = env[archiveKey];
404
+ return (
405
+ (typeof envUrl === 'string' &&
406
+ envUrl !== '' &&
407
+ (!hasPinnedEnvFile || envUrl !== fileUrl)) ||
408
+ (typeof envHash === 'string' &&
409
+ envHash !== '' &&
410
+ (!hasPinnedEnvFile || envHash !== fileHash)) ||
411
+ (typeof envArchive === 'string' &&
412
+ envArchive !== '' &&
413
+ (!hasPinnedEnvFile || envArchive !== fileArchive))
414
+ );
415
+ };
332
416
 
333
- // If a git submodule is present or source files and prebuilt executables are
334
- // both absent, consider ourselves to be in an active git checkout (as opposed
335
- // to e.g. an extracted npm tarball).
336
- const isWorkingCopy = hasGit || (!hasSource && !hasBin);
417
+ const hasSourceOverride =
418
+ hasExplicitOverride('MODDABLE_') || hasExplicitOverride('XSNAP_NATIVE_');
419
+ const needsPinnedRefresh =
420
+ hasRepoGit &&
421
+ (await Promise.all(sources.map(needsSourceRefresh))).some(needed => needed);
422
+ const shouldFetchSources =
423
+ hasSourceOverride || needsPinnedRefresh || (!hasSource && !hasBin);
337
424
 
338
- // --show-env reports submodule status without making changes.
425
+ // --show-env reports effective URL/hash pins without making changes.
339
426
  if (args.includes('--show-env')) {
340
- if (!isWorkingCopy) {
341
- throw Error('XSnap requires a working copy and git to --show-env');
342
- }
343
- await showEnv(submodules, { git, stdout });
427
+ await showEnv(sources, { stdout });
344
428
  return;
345
429
  }
346
430
 
347
- // Fetch/update source files via `git submodule` as appropriate.
348
- if (isWorkingCopy) {
349
- await updateSubmodules(submodules, { fs, git });
431
+ // Fetch/update source files via pinned source archives as appropriate.
432
+ if (shouldFetchSources) {
433
+ await updateSources(sources, { fs, curl, tar });
350
434
  }
351
435
 
352
436
  // If we now have source files, (re)build from them.
353
437
  // Otherwise, require presence of a previously-built executable.
354
- if (hasSource || isWorkingCopy) {
438
+ if (hasSource || shouldFetchSources) {
355
439
  // Force a rebuild if for some reason the binary is out of date
356
440
  // since the make checks may not always detect that situation.
357
441
  const npm = makeCLI('npm', { spawn });
@@ -374,9 +458,11 @@ const run = () =>
374
458
  spawn: childProcessTop.spawn,
375
459
  fs: {
376
460
  existsSync: fsTop.existsSync,
377
- rmdirSync: fsTop.rmdirSync,
378
461
  readFile: fsTop.promises.readFile,
379
462
  writeFile: fsTop.promises.writeFile,
463
+ mkdir: fsTop.promises.mkdir,
464
+ rm: fsTop.promises.rm,
465
+ rename: fsTop.promises.rename,
380
466
  },
381
467
  os: {
382
468
  type: osTop.type,