diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index caa34e7..d3da77e 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -63,7 +63,6 @@ jobs: libxkbcommon-dev \ libxkbcommon-x11-0 \ mesa-vulkan-drivers \ - blender \ mold \ pkg-config @@ -136,16 +135,45 @@ jobs: llvm-19 \ mesa-vulkan-drivers \ mold \ - blender \ pkg-config sudo update-alternatives --install /usr/bin/clang clang /usr/bin/clang-19 100 sudo update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-19 100 echo "/usr/lib/llvm-19/bin" >> "$GITHUB_PATH" rm -f "$HOME/.cache/cargo-xwin/clang-cl" + - name: Install pinned Blender + run: | + scripts/install-blender.sh "$RUNNER_TEMP/blender" + echo "BLENDER=$RUNNER_TEMP/blender/blender" >> "$GITHUB_ENV" + - name: Preprocess assets run: cargo run --locked --bin process-assets --no-default-features --features debug_features -j 4 + - name: Verify processed battlefield asset + run: | + python3 - <<'PY' + import json + import struct + + path = "imported_assets/Default/stages/battlefield/bf2.blend" + with open(path, "rb") as asset: + header = asset.read(12) + if len(header) != 12: + raise SystemExit(f"processed asset is truncated: {path}") + magic, version, length = struct.unpack("<4sII", header) + if magic != b"glTF" or version != 2 or length != asset.seek(0, 2): + raise SystemExit(f"processed asset is not a valid GLB: {path}") + asset.seek(12) + json_length, chunk_type = struct.unpack("&2 + exit 1 +fi + +if [[ -x "$install_dir/blender" ]]; then + "$install_dir/blender" --version | head -n 1 + exit 0 +fi + +mkdir -p "$(dirname "$install_dir")" +tmp_dir="$(mktemp -d)" +trap 'rm -rf "$tmp_dir"' EXIT + +archive="blender-${version}-linux-x64.tar.xz" +url="https://download.blender.org/release/Blender5.2/${archive}" + +curl --fail --location --silent --show-error --output "$tmp_dir/$archive" "$url" +printf '%s %s\n' "$sha256" "$tmp_dir/$archive" | sha256sum --check --status +tar --extract --xz --file "$tmp_dir/$archive" --directory "$tmp_dir" +mv "$tmp_dir/blender-${version}-linux-x64" "$install_dir" + +"$install_dir/blender" --version | head -n 1 diff --git a/src/blend_asset.rs b/src/blend_asset.rs index 5d45aa1..a6b3913 100644 --- a/src/blend_asset.rs +++ b/src/blend_asset.rs @@ -84,10 +84,11 @@ impl AssetLoader for BlendSourceLoader { .arg(&blend_path) .arg("--python") .arg(&script) - .arg("--") - .arg(&output) - .arg(&actions_path) - .output() + .arg("--") + .arg(&output) + .arg(&actions_path) + .arg(&blend_path) + .output() .map_err(|error| io::Error::other(format!("failed to start Blender: {error}")))?; if !result.status.success() { return Err(io::Error::other(format!( @@ -106,6 +107,7 @@ impl AssetLoader for BlendSourceLoader { let mut dependencies = manifest.dependencies; let document: serde_json::Value = serde_json::from_slice(&fs::read(&output)?) .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?; + validate_export(&document, &output)?; if let Some(images) = document.get("images").and_then(serde_json::Value::as_array) { for image in images { if let Some(uri) = image.get("uri").and_then(serde_json::Value::as_str) { @@ -142,6 +144,22 @@ impl AssetLoader for BlendSourceLoader { } } +fn validate_export(document: &serde_json::Value, output: &Path) -> io::Result<()> { + for key in ["scenes", "nodes", "meshes"] { + let count = document + .get(key) + .and_then(serde_json::Value::as_array) + .map_or(0, Vec::len); + if count == 0 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("Blender export {} contains no {key}", output.display()), + )); + } + } + Ok(()) +} + async fn referenced_actions( load_context: &mut LoadContext<'_>, root: &Path, diff --git a/src/blend_asset/export.py b/src/blend_asset/export.py index 61efb59..2c5e3ff 100644 --- a/src/blend_asset/export.py +++ b/src/blend_asset/export.py @@ -7,6 +7,15 @@ import sys def main(): output = sys.argv[sys.argv.index("--") + 1] actions_path = sys.argv[sys.argv.index("--") + 2] + source_path = sys.argv[sys.argv.index("--") + 3] + loaded_path = os.path.realpath(bpy.data.filepath) + expected_path = os.path.realpath(source_path) + if loaded_path != expected_path: + raise RuntimeError( + f"Blender loaded {loaded_path!r} instead of {expected_path!r}" + ) + if not bpy.data.objects or not bpy.data.meshes: + raise RuntimeError("Blender loaded an empty scene") with open(actions_path) as file: actions_to_keep = set(json.load(file))