diff --git a/.github/RELEASE.md b/.github/RELEASE.md index 858178bc..9061b9f2 100644 --- a/.github/RELEASE.md +++ b/.github/RELEASE.md @@ -42,3 +42,7 @@ Feature gates queued for stabilization (`@unstable` β†’ `@since`) must have passed their phase 3 vote, and any new dependence on a Component Model feature must have been adopted by a WASI Subgroup vote, before the release is cut. Both processes are documented in [CONTRIBUTING.md](../CONTRIBUTING.md). + +Adopted Component Model features are recorded in [Component Model features in +WASI](../docs/ComponentModelFeatures.md); features newly adopted for a release +must be called out in that release's notes. diff --git a/.github/actions/install-tools/action.yml b/.github/actions/install-tools/action.yml index 252bc6e7..2e190117 100644 --- a/.github/actions/install-tools/action.yml +++ b/.github/actions/install-tools/action.yml @@ -9,7 +9,7 @@ inputs: wasm-tools-version: description: 'Version of wasm-tools to install' required: false - default: '1.244.0' + default: '1.256.0' runs: using: 'composite' diff --git a/.github/scripts/README.md b/.github/scripts/README.md index 5cafc890..fe8afc16 100644 --- a/.github/scripts/README.md +++ b/.github/scripts/README.md @@ -1,5 +1,20 @@ # CI Scripts +## cm-features.js + +Reads the Component Model adoption record in `docs/ComponentModelFeatures.md`, +which is the single source of truth for which gated features WASI requires. + +- `publish.yml` runs it to write the "Component Model features" section of each + released `specifications/wasi-/Overview.md`. +- `validate-proposals.js` imports `readRecord` from it to build the + `wasm-tools validate` feature flags. + +```bash +# Print the section for a given version +node .github/scripts/cm-features.js 0.3.1 +``` + ## validate-proposals.js Validates WIT definitions for changed proposals. Used by the CI workflow. diff --git a/.github/scripts/cm-features.js b/.github/scripts/cm-features.js new file mode 100644 index 00000000..ea19fd5c --- /dev/null +++ b/.github/scripts/cm-features.js @@ -0,0 +1,137 @@ +#!/usr/bin/env node + +// Emit the "Component Model features" section of a specification overview. +// +// The adoption record in docs/ComponentModelFeatures.md is the source of +// truth: it lists every Component Model gated feature the WASI Subgroup has +// voted to adopt, and the release it was adopted for. Adoption is cumulative, +// so the section for a given version lists every feature adopted in that +// version or an earlier one. +// +// Prerelease versions (`0.3.0-rc-*`) are treated as their base version, since +// a release candidate carries the same feature baseline as the release it is +// a candidate for. +// +// Usage: node cm-features.js [record-path] + +const fs = require('fs'); +const path = require('path'); + +const RECORD = path.join(__dirname, '..', '..', 'docs', 'ComponentModelFeatures.md'); +const HEADING = '## Adopted features'; + +// The record marks the default (ungated) features with an em dash in the gate +// column. They are not a gated feature, so they are described in prose below +// rather than listed as a row. +const UNGATED = 'β€”'; + +function parseVersion(version) { + const base = version.split('-')[0]; + const parts = base.split('.').map((part) => Number(part)); + if (parts.length !== 3 || parts.some((part) => !Number.isInteger(part) || part < 0)) { + throw new Error(`not a version: '${version}'`); + } + return parts; +} + +function compareVersions(a, b) { + const left = parseVersion(a); + const right = parseVersion(b); + for (let i = 0; i < 3; i++) { + if (left[i] !== right[i]) return left[i] - right[i]; + } + return 0; +} + +// Rows of the adoption table, in document order, as {version, gate, feature}. +function readRecord(recordPath = RECORD) { + const lines = fs.readFileSync(recordPath, 'utf-8').split('\n'); + const start = lines.indexOf(HEADING); + if (start === -1) { + throw new Error(`${recordPath}: no '${HEADING}' heading`); + } + + const rows = []; + for (let i = start + 1; i < lines.length; i++) { + const line = lines[i].trim(); + if (line.startsWith('## ')) break; + if (!line.startsWith('|')) continue; + // Cells are read positionally, so a row missing its trailing pipe would + // silently lose the last character of its final cell. + if (!line.endsWith('|')) { + throw new Error(`${recordPath}:${i + 1}: table row does not end with '|'`); + } + + const cells = line.slice(1, -1).split('|').map((cell) => cell.trim()); + if (cells.length !== 4) { + throw new Error(`${recordPath}:${i + 1}: expected 4 columns, found ${cells.length}`); + } + // Skip the header and its separator, in any of the alignment styles. + if (cells[0] === 'WASI version') continue; + if (cells.every((cell) => /^:?-+:?$/.test(cell))) continue; + + rows.push({ version: cells[0], gate: cells[1], feature: cells[2] }); + } + + if (rows.length === 0) { + throw new Error(`${recordPath}: no rows under '${HEADING}'`); + } + return rows; +} + +// The section is appended to a partially written overview, ahead of that +// file's link definitions, so it both starts and ends with a blank line: a +// link definition on the line directly below a paragraph would be parsed as +// part of that paragraph. +function render(version, rows) { + const adopted = rows + .filter((row) => row.gate !== UNGATED) + .filter((row) => compareVersions(row.version, version) <= 0); + + const out = ['', '## Component Model features', '']; + + if (adopted.length === 0) { + out.push( + 'Implementing this version of the specification requires the default (ungated)', + '[Component Model][cm] features. No [gated features][gates] have been adopted.', + '', + '' + ); + return out.join('\n'); + } + + out.push( + 'Implementing this version of the specification requires the default (ungated)', + '[Component Model][cm] features, plus the [gated features][gates] adopted by the', + 'WASI Subgroup up to and including this version:', + '', + '| Gate | Feature | Adopted in |', + '| --- | --- | --- |' + ); + for (const row of adopted) { + out.push(`| ${row.gate} | ${row.feature} | ${row.version} |`); + } + out.push( + '', + 'These are required whether or not an API in this version uses them. See', + '[Component Model features in WASI][features] for the full adoption record.', + '', + '' + ); + return out.join('\n'); +} + +function main() { + const [version, recordPath = RECORD] = process.argv.slice(2); + if (!version) { + console.error('Usage: node cm-features.js [record-path]'); + process.exit(1); + } + process.stdout.write(render(version, readRecord(recordPath))); +} + +if (require.main === module) { + main(); +} + +module.exports = { RECORD, UNGATED, readRecord, render, compareVersions }; diff --git a/.github/scripts/validate-proposals.js b/.github/scripts/validate-proposals.js index 724e83d8..d058c0bb 100644 --- a/.github/scripts/validate-proposals.js +++ b/.github/scripts/validate-proposals.js @@ -2,7 +2,45 @@ const { execSync } = require('child_process'); const fs = require('fs'); +const os = require('os'); +const path = require('path'); const { validateDirectory, formatErrors } = require('./validate-since'); +const { readRecord, UNGATED } = require('./cm-features'); + +// `wasm-tools` feature name for each Component Model gate WASI has adopted. +// Adding a row to docs/ComponentModelFeatures.md without an entry here is a +// hard error rather than a silently un-enforced feature. +const GATE_FEATURES = { + 'πŸ”€': 'cm-async', + 'πŸ—ΊοΈ': 'cm-map', + '🏷️': 'cm-implements', +}; + +// Emoji gates carry a variation selector (U+FE0F) inconsistently across +// sources, so compare them without it. +const bare = (gate) => gate.replace(/\uFE0F/g, ''); + +// The adopted Component Model features, as `wasm-tools` CLI flags. Enabling +// them is what keeps proposals from depending on a feature the Subgroup has +// not voted to adopt: an un-adopted gate stays off and fails validation. Note +// these are added to whatever `wasm-tools` enables by default, so a gate that +// is un-adopted here but on by default there is not caught. +const adoptedFeatureFlags = () => { + const lookup = new Map(Object.entries(GATE_FEATURES).map(([g, f]) => [bare(g), f])); + const features = new Set(); + for (const { gate, version } of readRecord()) { + if (gate === UNGATED) continue; + const feature = lookup.get(bare(gate)); + if (!feature) { + throw new Error( + `docs/ComponentModelFeatures.md adopts '${gate}' for ${version}, but ` + + `GATE_FEATURES in ${path.basename(__filename)} has no wasm-tools feature name for it` + ); + } + features.add(feature); + } + return [...features].map((feature) => `-f ${feature}`).join(' '); +}; const parseFiles = (filesJson) => { if (!filesJson || filesJson === 'null') return []; @@ -48,6 +86,10 @@ if (toValidate.length === 0) { let failed = false; +const featureFlags = adoptedFeatureFlags(); +const outDir = fs.mkdtempSync(path.join(os.tmpdir(), 'wasi-validate-')); +console.log(`Component Model features: ${featureFlags || '(none adopted)'}`); + for (const proposal of toValidate) { const witDir = ((proposal) => `proposals/${proposal}/wit`)(proposal); console.log(`::group::Validating ${proposal}`); @@ -81,9 +123,16 @@ for (const proposal of toValidate) { // Validate wasm encoding console.log(' Validating wasm encoding...'); - if (!run(`wasm-tools component wit "${witDir}" --wasm -o /dev/null`)) { + const encoded = path.join(outDir, `${proposal}.wasm`); + if (!run(`wasm-tools component wit "${witDir}" --wasm -o "${encoded}"`)) { console.log(`::error::wasm encoding failed for ${proposal}`); failed = true; + } else if (!run(`wasm-tools validate ${featureFlags} "${encoded}"`)) { + // `component wit --wasm` does not enforce Component Model feature gates, + // so the encoded package is validated separately against the features + // WASI has adopted. + console.log(`::error::Component Model validation failed for ${proposal}`); + failed = true; } // Validate @since annotations @@ -99,6 +148,8 @@ for (const proposal of toValidate) { } } +fs.rmSync(outDir, { recursive: true, force: true }); + if (failed) { console.log('\n❌ Validation failed'); process.exit(1); diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 8436f92e..e6c511b1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -243,10 +243,18 @@ jobs: - [wasi:filesystem@${VERSION}](https://github.com/WebAssembly/WASI/pkgs/container/wasi%2Ffilesystem?tag=${VERSION}) - [wasi:cli@${VERSION}](https://github.com/WebAssembly/WASI/pkgs/container/wasi%2Fcli?tag=${VERSION}) - [wasi:http@${VERSION}](https://github.com/WebAssembly/WASI/pkgs/container/wasi%2Fhttp?tag=${VERSION}) + EOF + + # Which Component Model gated features this version requires, from + # the adoption record in docs/ComponentModelFeatures.md. + node .github/scripts/cm-features.js "$VERSION" >> "$SPEC_DIR/Overview.md" + cat >> "$SPEC_DIR/Overview.md" << EOF [cm]: https://github.com/WebAssembly/component-model [wit]: https://github.com/WebAssembly/component-model/blob/main/design/mvp/WIT.md [wasm-oci]: https://tag-runtime.cncf.io/wgs/wasm/deliverables/wasm-oci-artifact + [gates]: https://github.com/WebAssembly/component-model/blob/main/design/mvp/Explainer.md#gated-features + [features]: https://github.com/WebAssembly/WASI/blob/main/docs/ComponentModelFeatures.md EOF # Remove leading whitespace from heredoc diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 73d04a37..38935141 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -212,8 +212,10 @@ followed: evaluates whether the feature is genuinely stable and whether the iteration and feedback phase has been sufficient. 1. Once the SG votes to adopt the feature, stable (`@since`-gated) WASI APIs in - the next release may depend on it. The release notes for that version must - call out the newly adopted Component Model feature. + the next release may depend on it. The adoption is recorded in + [Component Model features in WASI](docs/ComponentModelFeatures.md), and the + release notes for that version must call out the newly adopted Component + Model feature. **Note:** Not all Component Model features are surfaced in WASI WIT definitions. Some, such as new canonical ABI features, have no WIT-level @@ -221,7 +223,9 @@ followed: 1. From that release onward, the feature is part of the baseline set of Component Model features required to implement that WASI version, matching the feature's status in the Component Model's [gated features] documentation - (for example, WASI 0.3 adopts the features gated by πŸ”€ async). + (for example, WASI 0.3 adopts the features gated by πŸ”€ async). A PR to the + Component Model repository updates that documentation to record which WASI + release adopted the feature. [Component Model]: https://github.com/WebAssembly/component-model [gated features]: https://github.com/WebAssembly/component-model/blob/main/design/mvp/Explainer.md#gated-features diff --git a/docs/ComponentModelFeatures.md b/docs/ComponentModelFeatures.md new file mode 100644 index 00000000..1fc39ee2 --- /dev/null +++ b/docs/ComponentModelFeatures.md @@ -0,0 +1,41 @@ +# Component Model features in WASI + +WASI APIs are defined in terms of the [Component Model], which develops new WIT +syntax, types, and canonical ABI functionality behind [gated features]. Stable +(`@since`-gated) WASI APIs may only depend on a gated feature after the WASI +Subgroup has voted to adopt it, following the process described in +[CONTRIBUTING.md](../CONTRIBUTING.md#adopting-component-model-features). + +This document records which features have been adopted, and the release they +were adopted for. Adoption is cumulative: implementing a given WASI version +requires the default (ungated) Component Model features, plus every feature +adopted in that version and in all earlier ones. + +Adopting a feature does not mean that any WASI API uses it. It means that WASI +APIs in that release and later *may* use it, and that runtimes and toolchains +which implement that WASI version must implement the feature regardless of +whether it is used yet. + +## Adopted features + +This table is the source for the "Component Model features" section of each +released [specification overview](../specifications). + +| WASI version | Gate | Feature | Adopted | +| --- | --- | --- | --- | +| 0.2.0 | β€” | The default (ungated) Component Model features | WASI 0.2 vote (predates this process) | +| 0.3.0 | πŸ”€ | `async` lift/lower, `future` and `stream` | WASI 0.3 vote (predates this process) | +| 0.3.1 | πŸ—ΊοΈ | The `map` type | [2026-08-06](https://github.com/WebAssembly/meetings/blob/main/wasi/2026/WASI-08-06.md) ([#943](https://github.com/WebAssembly/WASI/issues/943)) | +| 0.3.1 | 🏷️ | `implements` and `external-id` annotations on plain-named interface imports and exports | [2026-08-06](https://github.com/WebAssembly/meetings/blob/main/wasi/2026/WASI-08-06.md) ([#942](https://github.com/WebAssembly/WASI/issues/942)) | + +Features which have not been adopted are listed under [gated features] in the +Component Model Explainer. WASI proposals may experiment with them in +prerelease versions (such as `0.3.0-rc-*` releases), but stable releases must +remain implementable without them. + +CI enforces this: each changed proposal is encoded to a component and validated +with the features in the table above enabled, so a proposal that depends on an +un-adopted feature fails validation. + +[Component Model]: https://github.com/WebAssembly/component-model +[gated features]: https://github.com/WebAssembly/component-model/blob/main/design/mvp/Explainer.md#gated-features diff --git a/docs/Release.md b/docs/Release.md index 60d9d681..ec935a62 100644 --- a/docs/Release.md +++ b/docs/Release.md @@ -37,5 +37,8 @@ workflow which begin the release process. This process has the following steps: 1. Trigger the Release WASI workflow by running `gh workflow run release.yml -f prev_version=0.3. -f next_version=0.3.`. 1. A PR updating all version numbers is created. This is blocked on CI and review ([example](https://github.com/WebAssembly/WASI/pull/924)). 2. Create a GitHub release by running `gh release create "v0.3." --generate-notes`. + If the release adopts new Component Model features, the generated notes must + be edited to call them out. See [Component Model features in + WASI](ComponentModelFeatures.md). 3. Once the release has been created, a second PR will be automatically created to populate the `specifications/` dir ([example](https://github.com/WebAssembly/WASI/pull/925)). 4. Once this final PR is merged the release process is complete. diff --git a/specifications/wasi-0.3.0/Overview.md b/specifications/wasi-0.3.0/Overview.md index 3f5af04f..63392700 100644 --- a/specifications/wasi-0.3.0/Overview.md +++ b/specifications/wasi-0.3.0/Overview.md @@ -16,6 +16,21 @@ layout][wasm-oci]: - [wasi:cli@0.3.0](https://github.com/WebAssembly/WASI/pkgs/container/wasi%2Fcli?tag=0.3.0) - [wasi:http@0.3.0](https://github.com/WebAssembly/WASI/pkgs/container/wasi%2Fhttp?tag=0.3.0) +## Component Model features + +Implementing this version of the specification requires the default (ungated) +[Component Model][cm] features, plus the [gated features][gates] adopted by the +WASI Subgroup up to and including this version: + +| Gate | Feature | Adopted in | +| --- | --- | --- | +| πŸ”€ | `async`, `future` and `stream` | 0.3.0 | + +These are required whether or not an API in this version uses them. See +[Component Model features in WASI][features] for the full adoption record. + [cm]: https://github.com/WebAssembly/component-model [wit]: https://github.com/WebAssembly/component-model/blob/main/design/mvp/WIT.md [wasm-oci]: https://tag-runtime.cncf.io/wgs/wasm/deliverables/wasm-oci-artifact +[gates]: https://github.com/WebAssembly/component-model/blob/main/design/mvp/Explainer.md#gated-features +[features]: https://github.com/WebAssembly/WASI/blob/main/docs/ComponentModelFeatures.md