Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/fastly-compute-py/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ wac-graph = "0.8"
wac-parser = "0.8"
wit-parser = "0.244"
wit-component = "0.244"
wasm-metadata = { version = "0.245", default-features = false }
tempfile = "3"
componentize-py = { git = "https://github.com/bytecodealliance/componentize-py" }
futures = { version = "0.3", default-features = false, features = ["executor"] }
Expand All @@ -35,5 +36,6 @@ indexmap = "2"

[build-dependencies]
anyhow = "1"
cargo_metadata = "0.23.1"
wit-parser = "0.219"
wit-component = "0.219"
17 changes: 16 additions & 1 deletion crates/fastly-compute-py/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ fn main() -> Result<()> {
println!("cargo:rerun-if-changed=../../wit");
println!("cargo:rerun-if-changed=../../crates/wasiless");
println!("cargo:rerun-if-changed=../../wrap_app_in_wasiless.wac");
println!("cargo:rerun-if-changed=../../Cargo.lock");

let root_dir = PathBuf::from("../../");
let wit_dir = root_dir.join("wit");
Expand All @@ -25,6 +26,20 @@ fn main() -> Result<()> {
out_dir.join("wrap_app_in_wasiless.wac"),
)?;

// Expose the componentize-py version for embedding in Wasm producers metadata.
let metadata = cargo_metadata::MetadataCommand::new()
.manifest_path(root_dir.join("Cargo.toml"))
.exec()
.context("Failed to run `cargo metadata`")?;
let componentize_py_version = metadata
.packages
.iter()
.find(|p| p.name == "componentize-py")
.with_context(|| "componentize-py not found in cargo metadata")?
.version
.to_string();
println!("cargo:rustc-env=COMPONENTIZE_PY_VERSION={componentize_py_version}");

Ok(())
}

Expand Down Expand Up @@ -71,7 +86,7 @@ fn build_wasiless_wasm(root_dir: impl AsRef<Path>, out_dir: impl AsRef<Path>) ->
anyhow::bail!("Failed to build wasiless");
}

// Transform wasiless into a component using wasm-tools compnent new
// Transform wasiless into a component using wasm-tools component new
let input_wasm = target_dir.join("wasm32-unknown-unknown/release/wasiless.wasm");
let output_wasm = out_dir.as_ref().join("wasiless.wasm");

Expand Down
59 changes: 58 additions & 1 deletion crates/fastly-compute-py/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use wac_graph::EncodeOptions;
use wac_parser::Document;
use wac_types::BorrowedPackageKey;
use wac_types::Package;
use wasm_metadata::AddMetadata;

pub mod cli;
pub mod config;
Expand Down Expand Up @@ -168,7 +169,10 @@ pub fn build(output: PathBuf, entry_name: String, virtualenv: Option<PathBuf>) -
let composed =
compose_with_wasiless(&temp_component_wasm_path, WASILESS_WASM, WRAP_WAC, &output)?;

fs::write(&output, composed)
log::info!(" Injecting Fastly metadata...");
let annotated = inject_fastly_metadata(composed)?;

fs::write(&output, annotated)
.with_context(|| format!("Failed to write output: {}", output.display()))?;

log::debug!("Composed output: {}", output.display());
Expand Down Expand Up @@ -231,3 +235,56 @@ fn compose_with_wasiless(

Ok(encoded)
}

/// Inject build tool metadata into the Wasm component's standard `producers`
/// custom section.
///
/// Follows the WebAssembly [Producers Section spec] field conventions:
///
/// - `language: Python <version>` — the source language and the CPython
/// version bundled by componentize-py. Update this when upgrading to a
/// componentize-py release that bundles a different CPython version.
/// - `sdk: fastly-compute-py <version>` — the SDK library the user's code is
/// written against, analogous to `@fastly/js-compute` for the JS SDK.
/// - `processed-by: componentize-py <version>` — the tool that performed the
/// core Wasm transformation. `fastly-compute-py` also adds itself here as
/// the build orchestrator.
///
/// Note: the Fastly-proprietary `fastly.manifest.*` custom sections
/// (language, version, service_id, etc.) are **not** written here. Those are
/// injected during package ingestion, sourced from the `fastly.toml` manifest
/// that the CLI bundles alongside the Wasm in the upload package.
/// Dependency lists, build scripts, and machine info are similarly the CLI's
/// responsibility via its `fastly_data` producers entry.
///
/// [Producers Section spec]: https://github.com/WebAssembly/tool-conventions/blob/main/ProducersSection.md
fn inject_fastly_metadata(wasm: Vec<u8>) -> Result<Vec<u8>> {
let mut add_metadata = AddMetadata::default();

// Source language. The version is the CPython version bundled by
// componentize-py — update this when upgrading to a componentize-py
// release that bundles a different CPython version.
add_metadata
.language
.push(("Python".to_owned(), "3.14".to_owned()));

// The SDK the user's code is written against.
add_metadata.sdk.push((
"fastly-compute-py".to_owned(),
env!("CARGO_PKG_VERSION").to_owned(),
));

// Tools that performed the Wasm transformation.
add_metadata.processed_by.push((
"componentize-py".to_owned(),
env!("COMPONENTIZE_PY_VERSION").to_owned(),
));
add_metadata.processed_by.push((
"fastly-compute-py".to_owned(),
env!("CARGO_PKG_VERSION").to_owned(),
));

add_metadata
.to_wasm(&wasm)
.context("Failed to add producers metadata to Wasm component")
}
108 changes: 108 additions & 0 deletions fastly_compute/tests/test_wasm_metadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
"""Tests for Wasm producers metadata embedded in the built component.

Verifies that all top-level producers section fields written by
inject_fastly_metadata() are present and correct:

- language: Python 3.14
- sdk: fastly-compute-py 0.1.0
- processed-by: componentize-py 0.22.1
- processed-by: fastly-compute-py 0.1.0

The language version is also cross-checked against the libpython*.so name
embedded in the component tree to catch version drift when upgrading
componentize-py.
"""

import json
import re
import subprocess
from pathlib import Path

import pytest

WASM_PATH = Path("build/bottle-app.composed.wasm")

# NOTE: these will need to be updated at times but serves as a sanity check
FASTLY_COMPUTE_PY_VERSION = "0.1.0"
COMPONENTIZE_PY_VERSION = "0.22.1"
PYTHON_VERSION = "3.14"


@pytest.fixture(scope="module")
def metadata():
"""Return the parsed wasm-tools metadata JSON for the built component."""
if not WASM_PATH.exists():
pytest.fail(f"Built wasm not found at {WASM_PATH}; run `make` first")

result = subprocess.run(
["wasm-tools", "metadata", "show", "--json", str(WASM_PATH)],
capture_output=True,
text=True,
check=True,
)
return json.loads(result.stdout)


@pytest.fixture(scope="module")
def producers(metadata):
"""Return the top-level producers as a {field: {name: version}} dict."""
raw = metadata["component"]["metadata"]["producers"]
return dict(raw) if raw else {}


def _libpython_version(metadata) -> str | None:
"""Return the version from the first libpython*.so module name in the tree."""
nodes = [metadata]
while nodes:
node = nodes.pop()
kind = next(iter(node))
data = node[kind]
if kind == "module":
name = data.get("name") # may be None or ""
if name:
m = re.match(r"libpython(\d+\.\d+)\.so", name)
if m:
return m.group(1)
else:
nodes.extend(data.get("children", []))


def test_language_python(producers):
assert producers.get("language", {}).get("Python") == PYTHON_VERSION


def test_language_python_version_matches_libpython(producers, metadata):
"""Declared Python version must match the libpython*.so embedded by componentize-py.

If this fails, update PYTHON_VERSION in this file and the hardcoded version
in crates/fastly-compute-py/src/lib.rs. This is designed to keep us honest
and ensure that we don't end up injecting the wrong version as it is
not trivial to extract this from compnentize-py directly.
"""
embedded = _libpython_version(metadata)
assert embedded is not None, "No libpython*.so module found in component tree"
assert producers["language"]["Python"] == embedded, (
f"language: Python {producers['language']['Python']!r} does not match "
f"embedded libpython{embedded}.so — update PYTHON_VERSION in this file "
"and the hardcoded version in crates/fastly-compute-py/src/lib.rs"
)


def test_sdk_fastly_compute_py(producers):
assert (
producers.get("sdk", {}).get("fastly-compute-py") == FASTLY_COMPUTE_PY_VERSION
)


def test_processed_by_componentize_py(producers):
assert (
producers.get("processed-by", {}).get("componentize-py")
== COMPONENTIZE_PY_VERSION
)


def test_processed_by_fastly_compute_py(producers):
assert (
producers.get("processed-by", {}).get("fastly-compute-py")
== FASTLY_COMPUTE_PY_VERSION
)
Loading