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
5 changes: 4 additions & 1 deletion crates/compositor/src/audio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ extern "C" {

fn averr(ret: i32, ctx: &str) -> Result<()> {
if ret < 0 {
let mut buf = [0i8; 256];
// Surtout pas `[0i8; 256]` : `c_char` est signé sur x86_64 mais NON SIGNÉ sur
// aarch64, donc un i8 en dur fait du `*mut c_char` d'av_strerror une erreur de
// type sur arm64.
let mut buf = [0 as std::ffi::c_char; 256];
unsafe { av_strerror(ret, buf.as_mut_ptr(), buf.len()) };
let msg = unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) }.to_string_lossy();
bail!("{ctx}: {ret} ({msg})");
Expand Down
18 changes: 15 additions & 3 deletions electron/native/pipewire-capture/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,23 @@ fn freestanding_header_args() -> Vec<String> {
return Vec::new();
}
}
let gcc_root = Path::new("/usr/lib/gcc/x86_64-linux-gnu");
let Ok(entries) = std::fs::read_dir(gcc_root) else {
// The vendor triplet is NOT hardcoded. It is `x86_64-linux-gnu` on Debian/Ubuntu
// amd64 but `aarch64-linux-gnu` on arm64 — hardcoding the former is why bindgen
// died with "'limits.h' file not found" there — and neither on Arch
// (`x86_64-pc-linux-gnu`). Matching on the target arch prefix covers all of them,
// and on a box with a cross-gcc installed it still refuses the OTHER
// architecture's headers, whose type widths would be wrong.
let arch = std::env::var("CARGO_CFG_TARGET_ARCH")
.unwrap_or_else(|_| std::env::consts::ARCH.to_string());
let prefix = format!("{arch}-");
let Ok(vendors) = std::fs::read_dir("/usr/lib/gcc") else {
return Vec::new();
};
let mut dirs: Vec<PathBuf> = entries
let mut dirs: Vec<PathBuf> = vendors
.filter_map(|entry| entry.ok())
.filter(|entry| entry.file_name().to_string_lossy().starts_with(&prefix))
.filter_map(|vendor| std::fs::read_dir(vendor.path()).ok())
.flatten()
.filter_map(|entry| entry.ok())
.map(|entry| entry.path().join("include"))
.filter(|dir| dir.join("limits.h").is_file() && dir.join("stddef.h").is_file())
Expand Down
4 changes: 3 additions & 1 deletion electron/native/pipewire-capture/src/ffmpeg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ pub const AVIO_FLAG_WRITE: i32 = 2;
/// Formats an ffmpeg return code the way `av_err2str` would, since that too is a
/// macro and needs a caller-supplied buffer.
pub fn err_to_string(code: i32) -> String {
let mut buffer = [0i8; 256];
// NOT `[0i8; 256]`: `c_char` is signed on x86_64 but UNSIGNED on aarch64, so a
// hardcoded i8 makes av_strerror's `*mut c_char` a type error on arm64.
let mut buffer = [0 as std::ffi::c_char; 256];
// SAFETY: the buffer is the size we tell av_strerror it is, and ffmpeg
// always NUL-terminates within it.
let ok = unsafe { av_strerror(code, buffer.as_mut_ptr(), buffer.len()) } == 0;
Expand Down
30 changes: 25 additions & 5 deletions scripts/build-linux-compositor-addon.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,13 @@ function resolveFfmpegDir() {
);
}

/**
* Debian/Ubuntu multiarch triplet for the host. Hardcoding the x86_64 one made
* both libclang and gcc's stddef.h invisible on arm64, where the same libraries
* live under /usr/lib/aarch64-linux-gnu.
*/
const MULTIARCH = process.arch === "arm64" ? "aarch64-linux-gnu" : "x86_64-linux-gnu";

/**
* bindgen loads libclang at runtime. crates/.cargo/config.toml hardcodes a
* Windows LLVM path, so on Linux we locate it ourselves rather than making
Expand All @@ -88,7 +95,7 @@ function resolveLibclangDir() {
if (process.env.LIBCLANG_PATH) {
return process.env.LIBCLANG_PATH;
}
const roots = ["/usr/lib/x86_64-linux-gnu", "/usr/lib64", "/usr/lib"];
const roots = [`/usr/lib/${MULTIARCH}`, "/usr/lib64", "/usr/lib"];
for (const llvmRoot of ["/usr/lib"]) {
if (!fs.existsSync(llvmRoot)) continue;
for (const entry of fs.readdirSync(llvmRoot)) {
Expand All @@ -115,20 +122,33 @@ function resolveLibclangDir() {
* On distributions that ship only `libclang.so.1` (no -dev package) clang also
* cannot find its own `stddef.h`. Point it at gcc's copy in that case rather
* than making the caller discover it.
*
* Kept behaviourally identical to `freestanding_header_args()` in
* electron/native/pipewire-capture/build.rs, which is the authority — that one
* runs for a bare `cargo build` too. Two properties matter and both were once
* wrong here: `limits.h` must be required alongside `stddef.h` (the failure
* being fixed is `'limits.h' file not found` — glibc's copy `#include_next`s
* the compiler's), and the newest gcc must be picked deterministically rather
* than whatever `readdirSync` happened to return first.
*/
function bindgenClangArgs() {
if (process.env.BINDGEN_EXTRA_CLANG_ARGS) {
return process.env.BINDGEN_EXTRA_CLANG_ARGS;
}
const gccIncludeRoot = "/usr/lib/gcc/x86_64-linux-gnu";
const gccIncludeRoot = `/usr/lib/gcc/${MULTIARCH}`;
if (!fs.existsSync(gccIncludeRoot)) {
return "";
}
const withStddef = fs
const usable = fs
.readdirSync(gccIncludeRoot)
.map((version) => path.join(gccIncludeRoot, version, "include"))
.filter((dir) => fs.existsSync(path.join(dir, "stddef.h")));
return withStddef.length > 0 ? `-I${withStddef[0]}` : "";
.filter(
(dir) =>
fs.existsSync(path.join(dir, "limits.h")) && fs.existsSync(path.join(dir, "stddef.h")),
)
.sort()
.reverse();
return usable.length > 0 ? `-I${usable[0]}` : "";
}

/**
Expand Down
6 changes: 6 additions & 0 deletions scripts/build-linux-pipewire-helper.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ if (cargoVersion.status !== 0) {
process.exit(1);
}

// No BINDGEN_EXTRA_CLANG_ARGS wrangling here on purpose. bindgen needs clang's
// freestanding headers (limits.h, stddef.h) and Ubuntu ships libclang.so.1 with
// no resource dir, but the crate's own build.rs already falls back to gcc's
// copies — see freestanding_header_args() there. Doing it here too would only
// cover the build that goes through this script and leave a bare `cargo build`
// broken, which is exactly the split build.rs exists to avoid.
const build = spawnSync("cargo", ["build", "--release", "--manifest-path", manifest], {
cwd: crateDir,
stdio: "inherit",
Expand Down
4 changes: 4 additions & 0 deletions scripts/fetch-ffmpeg.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ const SHARED_PINNED = {
asset: "ffmpeg-n8.1.2-34-g9b6c8969e0-linux64-lgpl-shared-8.1.tar.xz",
sha256: "c882a80f06617149198a98a07a0880a7e881953ae9f9cb931f5be09a4f93caae",
},
"linux-arm64": {
asset: "ffmpeg-n8.1.2-34-g9b6c8969e0-linuxarm64-lgpl-shared-8.1.tar.xz",
sha256: "eec386482ac6799bb547b5f507dedd19ef6354eee0ca4ddb04bdd053d03c3cfb",
},
"win32-x64": {
asset: "ffmpeg-n8.1.2-34-g9b6c8969e0-win64-lgpl-shared-8.1.zip",
sha256: "c222a490dde4e7059f45495deef6bfb98dbcacc2b43df5b607546252037aa95c",
Expand Down
71 changes: 71 additions & 0 deletions scripts/fetch-ffmpeg.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// The pin table in fetch-ffmpeg.mjs is a supply-chain artifact, and that file says
// so itself: "tag, asset name and digest are one unit. Re-pin all three or none."
// Nothing enforced it. A re-pin that moved some entries and not others merges clean
// — the edits do not even touch the same lines — and then fails only on the arch
// nobody builds in CI, where `npm run build:linux` dies on a 404 from the pinned
// release before it has done anything.
//
// That is not hypothetical: the linux-arm64 shared entry arrived naming
// n8.1.2-32-gcfa62de001 while every sibling had already moved to
// n8.1.2-34-g9b6c8969e0. This test is the guard that was missing.
//
// Read as source text rather than imported: fetch-ffmpeg.mjs calls main() at import
// and would start downloading. The property under test is a property of the literal
// table anyway.

import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";

const source = fs.readFileSync(
path.join(path.dirname(fileURLToPath(import.meta.url)), "fetch-ffmpeg.mjs"),
"utf8",
);

// Anchored at the property so prose in the file's (extensive) comments cannot match.
const assets = [...source.matchAll(/^\s*asset:\s*"([^"]+)"/gm)].map((m) => m[1]);
const digests = [...source.matchAll(/^\s*sha256:\s*"([^"]+)"/gm)].map((m) => m[1]);

/** `ffmpeg-n8.1.2-34-g9b6c8969e0-linuxarm64-lgpl-shared-8.1.tar.xz` -> `n8.1.2-34-g9b6c8969e0`. */
const buildId = (asset) => asset.match(/-(n\d+(?:\.\d+)+-\d+-g[0-9a-f]+)-/)?.[1];

describe("fetch-ffmpeg pins", () => {
// Without this, a reformat that breaks the regexes above would leave every other
// assertion iterating an empty array and passing vacuously.
it("still finds the pin table", () => {
expect(assets.length).toBeGreaterThanOrEqual(8);
expect(digests).toHaveLength(assets.length);
});

it("names one single ffmpeg build across every pinned asset", () => {
const byBuild = new Map();
for (const asset of assets) {
const id = buildId(asset);
// Also the "never an `N-…` master snapshot" rule: those carry no n<version>.
expect(id, `${asset} does not name an n<version>-<n>-g<sha> release build`).toBeDefined();
byBuild.set(id, [...(byBuild.get(id) ?? []), asset]);
}
expect(
[...byBuild.keys()],
`The pin table straddles ${byBuild.size} different ffmpeg builds:\n${[...byBuild]
.map(([id, list]) => ` ${id}\n${list.map((a) => ` ${a}`).join("\n")}`)
.join("\n")}\nRe-pin every entry together — RELEASE_TAG, asset and digest are one unit.`,
).toHaveLength(1);
});

// One GPL component relicenses the whole binary, which would contaminate this MIT
// app. assertLgpl() checks the artifact after download; this checks the intent
// before anyone runs the script.
it("pins only -lgpl assets", () => {
for (const asset of assets) {
expect(asset, `${asset} is not an -lgpl asset`).toMatch(/-lgpl(-shared)?-/);
}
});

it("pins a full sha-256 for every asset", () => {
for (const digest of digests) {
expect(digest).toMatch(/^[0-9a-f]{64}$/);
}
});
});
2 changes: 1 addition & 1 deletion vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default defineConfig({
// docblock the other way round, to escape the global jsdom; that one is now
// redundant but harmless.)
environment: "node",
include: ["{src,electron,.github}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
include: ["{src,electron,scripts,.github}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
// Vitest's 5s default is too tight here and produces red runs that mean nothing.
// Measured: with the machine loaded, 11 tests fail and 9 of them are purely
// "Test timed out in 5000ms" — ordinary component tests that pass in 200ms on an
Expand Down
Loading