@agoric/deploy-script-support
Version:
Helpers and other support for writing deploy scripts
314 lines (286 loc) • 10.9 kB
JavaScript
// @ts-check
import { Fail } from '@endo/errors';
import { deeplyFulfilledObject } from '@agoric/internal';
import fs from 'fs';
import { createRequire } from 'module';
import path from 'path';
import { defangAndTrim, stringify } from './code-gen.js';
import {
makeCoreProposalBehavior,
makeEnactCoreProposalsFromBundleRef,
} from './coreProposalBehavior.js';
/**
* @typedef {string | {module: string, entrypoint?: string, args?: Array<unknown>}} ConfigProposal
*/
/**
* @typedef {{steps: ConfigProposal[][]}} SequentialCoreProposals
* @typedef {ConfigProposal[] | SequentialCoreProposals} CoreProposals
*/
const req = createRequire(import.meta.url);
/**
* @param {...(CoreProposals | undefined | null)} args
* @returns {SequentialCoreProposals}
*/
export const mergeCoreProposals = (...args) => {
/** @type {ConfigProposal[][]} */
const steps = [];
for (const coreProposal of args) {
if (!coreProposal) {
continue;
}
if ('steps' in coreProposal) {
steps.push(...coreProposal.steps);
} else {
steps.push(coreProposal);
}
}
return harden({ steps });
};
harden(mergeCoreProposals);
/**
* @param {(ModuleSpecifier | FilePath)[]} paths
* @typedef {string} ModuleSpecifier
* @typedef {string} FilePath
*/
const pathResolve = (...paths) => {
const fileName = /** @type {string} */ (paths.pop());
fileName || Fail`base name required`;
try {
return req.resolve(fileName, {
paths,
});
} catch (e) {
return path.resolve(...paths, fileName);
}
};
const findModule = (initDir, srcSpec) =>
srcSpec.match(/^(\.\.?)?\//)
? pathResolve(initDir, srcSpec)
: req.resolve(srcSpec);
/**
* @param {{ bundleID?: string, bundleName?: string }} handle - mutated then hardened
* @param {string} sourceSpec - the specifier of a module to load
* @param {string} key - the key of the proposal
* @param {string} piece - the piece of the proposal
* @returns {Promise<[string, any]>}
*/
const namedHandleToBundleSpec = async (handle, sourceSpec, key, piece) => {
handle.bundleName = `coreProposal${String(key)}_${piece}`;
harden(handle);
return harden([handle.bundleName, { sourceSpec }]);
};
/**
* Format core proposals to be run at bootstrap:
* SwingSet `bundles` configuration
* and `code` to execute them, interpolating functions
* such as `makeCoreProposalBehavior`.
*
* Core proposals are proposals for use with swingset-core-eval.
* In production, they are triggered by BLD holder governance decisions,
* but for sim-chain and such, they can be declared statically in
* the chain configuration, in which case they are run at bootstrap.
*
* @param {CoreProposals} coreProposals - governance
* proposals to run at chain bootstrap for scenarios such as sim-chain.
* @param {FilePath} [dirname]
* @param {object} [opts]
* @param {typeof makeEnactCoreProposalsFromBundleRef} [opts.makeEnactCoreProposals]
* @param {(key: PropertyKey) => PropertyKey} [opts.getSequenceForProposal]
* @param {typeof namedHandleToBundleSpec} [opts.handleToBundleSpec]
*/
export const extractCoreProposalBundles = async (
coreProposals,
dirname = '.',
opts,
) => {
const {
makeEnactCoreProposals = makeEnactCoreProposalsFromBundleRef,
getSequenceForProposal = key => key,
handleToBundleSpec = namedHandleToBundleSpec,
} = opts || {};
dirname = pathResolve(dirname);
dirname = await fs.promises
.stat(dirname)
.then(stbuf => (stbuf.isDirectory() ? dirname : path.dirname(dirname)));
/** @type {Map<{ bundleID?: string, bundleName?: string }, { source: string, bundle?: string }>} */
const bundleHandleToAbsolutePaths = new Map();
const proposalSteps =
'steps' in coreProposals ? coreProposals.steps : [coreProposals];
const bundleToSource = new Map();
const extractedSteps = await Promise.all(
proposalSteps.map((proposalStep, i) =>
Promise.all(
proposalStep.map(async (coreProposal, j) => {
const key = `${i}.${j}`;
// console.debug(`Parsing core proposal:`, coreProposal);
/** @type {string} */
let entrypoint;
/** @type {unknown[]} */
let args;
/** @type {string} */
let module;
if (typeof coreProposal === 'string') {
module = coreProposal;
entrypoint = 'defaultProposalBuilder';
args = [];
} else {
({
module,
entrypoint = 'defaultProposalBuilder',
args = [],
} = coreProposal);
}
typeof module === 'string' ||
Fail`coreProposal module ${module} must be string`;
typeof entrypoint === 'string' ||
Fail`coreProposal entrypoint ${entrypoint} must be string`;
Array.isArray(args) || Fail`coreProposal args ${args} must be array`;
const thisProposalBundleHandles = new Set();
assert(getSequenceForProposal);
const thisProposalSequence = getSequenceForProposal(key);
const initPath = findModule(dirname, module);
const initDir = path.dirname(initPath);
/** @type {Record<string, import('./externalTypes.js').CoreEvalBuilder>} */
const ns = await import(initPath);
const install = (srcSpec, bundlePath) => {
const absoluteSrc = findModule(initDir, srcSpec);
const bundleHandle = {};
const absolutePaths = { source: absoluteSrc };
if (bundlePath) {
const absoluteBundle = pathResolve(initDir, bundlePath);
absolutePaths.bundle = absoluteBundle;
const oldSource = bundleToSource.get(absoluteBundle);
if (oldSource) {
oldSource === absoluteSrc ||
Fail`${bundlePath} already installed from ${oldSource}, now ${absoluteSrc}`;
} else {
bundleToSource.set(absoluteBundle, absoluteSrc);
}
}
// Don't harden the bundleHandle since we need to set the bundleName on
// its unique identity later.
thisProposalBundleHandles.add(bundleHandle);
bundleHandleToAbsolutePaths.set(
bundleHandle,
harden(absolutePaths),
);
return bundleHandle;
};
/** @type {import('./externalTypes.js').PublishBundleRef} */
const publishRef = async handleP => {
const handle = await handleP;
bundleHandleToAbsolutePaths.has(handle) ||
Fail`${handle} not in installed bundles`;
return handle;
};
const proposal = await ns[entrypoint](
{
publishRef,
// @ts-expect-error not statically verified to return a full obj
install,
},
...args,
);
// Add the proposal bundle handles in sorted order.
const bundleSpecEntries = await Promise.all(
[...thisProposalBundleHandles.keys()]
.map(handle => [handle, bundleHandleToAbsolutePaths.get(handle)])
.sort(([_hnda, { source: a }], [_hndb, { source: b }]) => {
if (a < b) {
return -1;
}
if (a > b) {
return 1;
}
return 0;
})
.map(async ([handle, absolutePaths], k) => {
// Transform the bundle handle identity into a bundleName reference.
const specEntry = await handleToBundleSpec(
handle,
absolutePaths.source,
thisProposalSequence,
String(k),
);
harden(handle);
return specEntry;
}),
);
// Now that we've assigned all the bundleNames and hardened the
// handles, we can extract the behavior bundle.
const { sourceSpec, getManifestCall } = await deeplyFulfilledObject(
harden(proposal),
);
const proposalSource = pathResolve(initDir, sourceSpec);
const proposalNS = await import(proposalSource);
const [manifestGetterName, ...manifestGetterArgs] = getManifestCall;
manifestGetterName in proposalNS ||
Fail`proposal ${proposalSource} missing export ${manifestGetterName}`;
const { manifest: customManifest } = await proposalNS[
manifestGetterName
](harden({ restoreRef: () => null }), ...manifestGetterArgs);
const behaviorBundleHandle = {};
const specEntry = await handleToBundleSpec(
behaviorBundleHandle,
proposalSource,
thisProposalSequence,
'proposalNS',
);
bundleSpecEntries.unshift(specEntry);
bundleHandleToAbsolutePaths.set(
behaviorBundleHandle,
harden({
source: proposalSource,
}),
);
return /** @type {const} */ ([
key,
{
ref: behaviorBundleHandle,
call: getManifestCall,
customManifest,
bundleSpecs: bundleSpecEntries,
},
]);
}),
),
),
);
// Extract all the bundle specs in already-sorted order.
const bundles = Object.fromEntries(
extractedSteps.flatMap(step =>
step.flatMap(([_key, { bundleSpecs }]) => bundleSpecs),
),
);
harden(bundles);
const codeSteps = extractedSteps.map(extractedStep => {
// Extract the manifest references and calls.
const metadataRecords = extractedStep.map(([_key, extractedSpec]) => {
const { ref, call, customManifest } = extractedSpec;
return { ref, call, customManifest };
});
harden(metadataRecords);
const code = `\
// This is generated by @agoric/deploy-script-support/src/extract-proposal.js - DO NOT EDIT
/* eslint-disable */
const metadataRecords = harden(${stringify(metadataRecords, true)});
// Make an enactCoreProposals function and "export" it by way of script completion value.
// It is constructed by an IIFE to ensure the absence of global bindings for
// makeCoreProposalBehavior and makeEnactCoreProposals (the latter referencing the former),
// which may not be necessary but preserves behavior pre-dating
// https://github.com/Agoric/agoric-sdk/pull/8712 .
const enactCoreProposals = ((
makeCoreProposalBehavior = ${makeCoreProposalBehavior},
makeEnactCoreProposals = ${makeEnactCoreProposals},
) => makeEnactCoreProposals({ metadataRecords, E }))();
enactCoreProposals;
`;
return defangAndTrim(code);
});
// console.debug('created bundles from proposals:', coreProposals, bundles);
return harden({
bundles,
codeSteps,
bundleHandleToAbsolutePaths,
});
};