build examples script in rust with wasm-opt checks

This commit is contained in:
Matt Yan 2025-03-01 18:22:40 +09:00 committed by Siyuan Yan
parent 72b4c0ed9d
commit a0d4ab0fc8
8 changed files with 321 additions and 45 deletions

View File

@ -4,6 +4,7 @@ on:
branches: [master]
paths:
- 'ci/**'
- 'tools/build-examples/**'
- 'examples/**'
jobs:
@ -33,7 +34,7 @@ jobs:
version: 'latest'
- name: Build examples
run: ./ci/build-examples.sh
run: cargo run -p build-examples -b build-examples
- name: Deploy to Firebase
uses: siku2/action-hosting-deploy@v1

8
Cargo.lock generated
View File

@ -302,6 +302,14 @@ dependencies = [
"yew",
]
[[package]]
name = "build-examples"
version = "0.1.0"
dependencies = [
"regex",
"reqwest",
]
[[package]]
name = "bumpalo"
version = "3.17.0"

View File

@ -1,43 +0,0 @@
#!/usr/bin/env bash
# Must be run from root of the repo:
# yew $ ./ci/build-examples.sh
output="$(pwd)/dist"
mkdir -p "$output"
failure=false
for path in examples/*; do
if [[ ! -d $path ]]; then
continue
fi
example=$(basename "$path")
# ssr does not need trunk
if [[ "$example" == "simple_ssr" || "$example" == "ssr_router" ]]; then
continue
fi
echo "::group::Building $example"
if ! (
set -e
# we are sure that $path exists
# shellcheck disable=SC2164
cd "$path"
dist_dir="$output/$example"
export RUSTFLAGS="--cfg nightly_yew --cfg getrandom_backend=\"wasm_js\""
trunk build --release --dist "$dist_dir" --public-url "$PUBLIC_URL_PREFIX/$example" --no-sri
# check that there are no undefined symbols. Those generate an import .. from 'env',
# which isn't available in the browser.
{ cat "$dist_dir"/*.js | grep -q -e "from 'env'" ; } && exit 1 || true
) ; then
echo "::error ::$example failed to build"
failure=true
fi
echo "::endgroup::"
done
if [ "$failure" = true ] ; then
exit 1
fi

View File

@ -2,7 +2,7 @@
#![doc(html_logo_url = "https://yew.rs/img/logo.png")]
#![cfg_attr(documenting, feature(doc_cfg))]
#![cfg_attr(documenting, feature(doc_auto_cfg))]
#![cfg_attr(nightly_yew, feature(fn_traits, async_closure, unboxed_closures))]
#![cfg_attr(nightly_yew, feature(fn_traits, unboxed_closures))]
//! # Yew Framework - API Documentation
//!

View File

@ -0,0 +1,10 @@
[package]
name = "build-examples"
version = "0.1.0"
edition = "2024"
publish = false
[dependencies]
reqwest = { version = "0.12.12", features = ["blocking"] }
regex = "1.11.1"

View File

@ -0,0 +1,124 @@
use std::fs;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use build_examples::{get_latest_wasm_opt_version, is_wasm_opt_outdated, NO_TRUNK_EXAMPLES};
use regex::Regex;
fn main() -> ExitCode {
// Must be run from root of the repo
let examples_dir = Path::new("examples");
if !examples_dir.exists() {
eprintln!(
"examples directory not found. Make sure you're running from the root of the repo."
);
return ExitCode::from(1);
}
let latest_wasm_opt = get_latest_wasm_opt_version();
let mut outdated_example_paths = Vec::new();
let mut outdated_examples = Vec::new();
// Get all entries in the examples directory
let entries = fs::read_dir(examples_dir).expect("Failed to read examples directory");
for entry in entries {
let entry = entry.expect("Failed to read directory entry");
let path = entry.path();
// Skip if not a directory
if !path.is_dir() {
continue;
}
let example = path
.file_name()
.expect("Failed to get directory name")
.to_string_lossy()
.to_string();
// Skip ssr examples as they don't need trunk
if NO_TRUNK_EXAMPLES.contains(&example.as_str()) {
continue;
}
// Check Trunk.toml for wasm_opt version and collect outdated examples
if is_wasm_opt_outdated(&path, &latest_wasm_opt) {
outdated_examples.push(example);
outdated_example_paths.push(path);
}
}
if outdated_examples.is_empty() {
println!(
"All examples are up-to-date with the latest wasm_opt version: {}",
latest_wasm_opt
);
return ExitCode::from(0);
}
println!(
"Found {} examples with outdated or missing wasm_opt configuration:",
outdated_examples.len()
);
for example in &outdated_examples {
println!(" - {}", example);
}
println!("Latest wasm_opt version is: {}", latest_wasm_opt);
println!("Updating all examples...");
let updated_count = update_all_examples(&outdated_example_paths, &latest_wasm_opt);
println!(
"Updated {} example configurations to use {}",
updated_count, latest_wasm_opt
);
ExitCode::from(0)
}
pub fn update_all_examples(outdated_paths: &[PathBuf], latest_version: &str) -> usize {
let mut updated_count = 0;
let re = Regex::new(r#"(?m)^\[tools\]\s*\nwasm_opt\s*=\s*"(version_\d+)""#).unwrap();
for path in outdated_paths {
let trunk_toml_path = path.join("Trunk.toml");
let content = fs::read_to_string(&trunk_toml_path).unwrap_or_default();
let updated_content = if re.is_match(&content) {
// Replace existing wasm_opt version
re.replace(&content, |_: &regex::Captures| {
format!(
r#"[tools]
wasm_opt = "{}""#,
latest_version
)
})
.to_string()
} else {
// Add wasm_opt configuration
if content.is_empty() {
format!(
r#"[tools]
wasm_opt = "{}""#,
latest_version
)
} else {
format!(
"{}\n\n[tools]\nwasm_opt = \"{}\"",
content.trim(),
latest_version
)
}
};
if let Err(e) = fs::write(&trunk_toml_path, updated_content) {
println!("Failed to update {}: {}", trunk_toml_path.display(), e);
} else {
updated_count += 1;
}
}
updated_count
}

View File

@ -0,0 +1,44 @@
use std::fs;
use std::path::Path;
use regex::Regex;
/// Examples that don't use Trunk for building
pub const NO_TRUNK_EXAMPLES: [&str; 3] = ["simple_ssr", "ssr_router", "wasi_ssr_module"];
pub fn get_latest_wasm_opt_version() -> String {
let url = "https://github.com/WebAssembly/binaryen/releases";
let client = reqwest::blocking::Client::new();
let res = client.get(url).send().unwrap();
let body = res.text().unwrap();
let re = Regex::new(r#"version_(\d+)"#).unwrap();
let captures = re.captures_iter(&body);
let mut versions: Vec<u32> = captures
.map(|c| c.get(1).unwrap().as_str().parse().unwrap())
.collect();
versions.sort();
format!("version_{}", versions.last().unwrap())
}
pub fn is_wasm_opt_outdated(path: &Path, latest_version: &str) -> bool {
let trunk_toml_path = path.join("Trunk.toml");
if !trunk_toml_path.exists() {
return true;
}
let content = match fs::read_to_string(&trunk_toml_path) {
Ok(content) => content,
Err(_) => return true,
};
// Check if wasm_opt is configured and up-to-date
let re = Regex::new(r#"(?m)^\[tools\]\s*\nwasm_opt\s*=\s*"(version_\d+)""#).unwrap();
match re.captures(&content) {
Some(captures) => {
let current_version = captures.get(1).unwrap().as_str();
current_version != latest_version
}
None => true,
}
}

View File

@ -0,0 +1,132 @@
use std::path::Path;
use std::process::{Command, ExitCode};
use std::{env, fs};
use build_examples::{get_latest_wasm_opt_version, is_wasm_opt_outdated, NO_TRUNK_EXAMPLES};
fn main() -> ExitCode {
// Must be run from root of the repo:
// yew $ cargo r -p build-examples -b build-examples
let output_dir = env::current_dir().expect("Failed to get current directory");
let output_dir = output_dir.join("dist");
fs::create_dir_all(&output_dir).expect("Failed to create output directory");
let examples_dir = Path::new("examples");
if !examples_dir.exists() {
eprintln!(
"examples directory not found. Make sure you're running from the root of the repo."
);
return ExitCode::from(1);
}
let mut failure = false;
let latest_wasm_opt = get_latest_wasm_opt_version();
let mut outdated_examples = Vec::new();
let mut outdated_example_paths = Vec::new();
// Get all entries in the examples directory
let entries = fs::read_dir(examples_dir).expect("Failed to read examples directory");
for entry in entries {
let entry = entry.expect("Failed to read directory entry");
let path = entry.path();
// Skip if not a directory
if !path.is_dir() {
continue;
}
let example = path
.file_name()
.expect("Failed to get directory name")
.to_string_lossy()
.to_string();
// Skip ssr examples as they don't need trunk
if NO_TRUNK_EXAMPLES.contains(&example.as_str()) {
continue;
}
// Check Trunk.toml for wasm_opt version and collect outdated examples
if is_wasm_opt_outdated(&path, &latest_wasm_opt) {
outdated_examples.push(example.clone());
outdated_example_paths.push(path.clone());
}
println!("::group::Building {}", example);
if !build_example(&path, &output_dir, &example) {
eprintln!("::error ::{} failed to build", example);
failure = true;
}
println!("::endgroup::");
}
// Emit warning if any examples have outdated wasm_opt
if !outdated_examples.is_empty() {
println!(
"::warning ::{} example crates do not have up-to-date wasm_opt: {}",
outdated_examples.len(),
outdated_examples.join(", ")
);
}
if failure {
ExitCode::from(1)
} else {
ExitCode::from(0)
}
}
fn build_example(path: &Path, output_dir: &Path, example: &str) -> bool {
// Get the public URL prefix from environment or use default
let public_url_prefix = env::var("PUBLIC_URL_PREFIX").unwrap_or_default();
// Set up the dist directory for this example
let dist_dir = output_dir.join(example);
// Run trunk build command
let status = Command::new("trunk")
.current_dir(path)
.arg("build")
.env(
"RUSTFLAGS",
"--cfg nightly_yew --cfg getrandom_backend=\"wasm_js\"",
)
.arg("--release")
.arg("--dist")
.arg(&dist_dir)
.arg("--public-url")
.arg(format!("{}/{}", public_url_prefix, example))
.arg("--no-sri")
.status();
match status {
Ok(status) if status.success() => {
// Check for undefined symbols (imports from 'env')
let js_files = match fs::read_dir(&dist_dir) {
Ok(entries) => entries
.filter_map(Result::ok)
.filter(|e| e.path().extension().is_some_and(|ext| ext == "js"))
.collect::<Vec<_>>(),
Err(_) => return false,
};
for js_file in js_files {
let content = match fs::read_to_string(js_file.path()) {
Ok(content) => content,
Err(_) => return false,
};
if content.contains("from 'env'") {
return false;
}
}
true
}
_ => false,
}
}