The browser could not build from a clean clone on Windows: the fetch was a bash script needing python3 and tar. It is Rust in build.rs now: the spotify index is read by a strict scanner, the newest stable standard build for the platform is picked (or MAKEPAD_CEF_VERSION pins one), the archive is downloaded to a .part file with its size and sha1 checked, extracted beside it, and a current-<platform>.txt pointer file names the dist — read before the old symlink, so the Mac keeps its pinned 138 while a fresh box gets the current build; a platform that already has a dist is never bumped. MAKEPAD_CEF_OFFLINE refuses with the dir and pointer named; MAKEPAD_CEF_DRY_RUN prints the plan (with MAKEPAD_CEF_PLATFORM to resolve another host's). Build-time deps only: ureq (rustls), bzip2 (bundled), tar, sha1_smol. The shell script is gone. Proven on this Mac: a pinned re-download of the 138 macosarm64 archive (255 MB, sha1 verified) differs in nothing from the existing dist over 1,193 files; dry runs for linux64 and windows64 pick the archives already on disk; the browser checks with no download; the cef crate's build deps compile for the windows-msvc and linux-gnu hosts. Picker and pointer tests 5. An actual download on Windows or Linux is not yet run. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
660 lines
27 KiB
Rust
660 lines
27 KiB
Rust
//! Where the CEF distribution comes from when the build has none: the
|
|
//! Spotify CDN's index of official builds, read and unpacked by the build
|
|
//! script itself — in Rust, on every host, with no shell, python or tar
|
|
//! around it. A clean clone builds the browser on Windows as on the Mac.
|
|
//!
|
|
//! The pieces, each a plain function so the picker is testable without a
|
|
//! network:
|
|
//!
|
|
//! - [`pick`]: the index (`{platform: {versions: [{cef_version, channel,
|
|
//! files: [{type, name, sha1, size}]}]}}`, newest first) → the newest
|
|
//! `stable` `standard` archive for a platform, or the one a pin names.
|
|
//! - [`Pointer`]: which extracted directory `current-<platform>` means. The
|
|
//! shell script used a symlink; a text file beside it works on every
|
|
//! filesystem without privileges, so the build reads the file first and
|
|
//! the symlink only as the older form.
|
|
//! - [`ensure_dist`]: the whole thing — pointer, else download, verify,
|
|
//! extract, write the pointer. A platform that already has a dist is
|
|
//! never bumped: the CDN moves on, our pin does not.
|
|
//!
|
|
//! The index is 10 MB of JSON; this reads exactly the fields it needs with
|
|
//! a small strict scanner rather than pulling a JSON crate into a build
|
|
//! script. Anything shaped differently is an error, never a guess.
|
|
|
|
use std::fs;
|
|
use std::io::{Read, Write};
|
|
use std::path::{Path, PathBuf};
|
|
|
|
pub const INDEX_URL: &str = "https://cef-builds.spotifycdn.com/index.json";
|
|
pub const BASE_URL: &str = "https://cef-builds.spotifycdn.com";
|
|
|
|
/// The platforms the CDN builds and we link. `linux32` and friends exist
|
|
/// there too, but nothing of ours runs on them.
|
|
pub const PLATFORMS: &[&str] = &[
|
|
"linux64",
|
|
"linuxarm64",
|
|
"macosarm64",
|
|
"macosx64",
|
|
"windows64",
|
|
"windowsarm64",
|
|
];
|
|
|
|
/// One downloadable archive, as the index describes it.
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct Archive {
|
|
pub cef_version: String,
|
|
pub channel: String,
|
|
pub name: String,
|
|
pub sha1: String,
|
|
pub size: u64,
|
|
}
|
|
|
|
impl Archive {
|
|
pub fn url(&self) -> String {
|
|
format!("{BASE_URL}/{}", self.name)
|
|
}
|
|
|
|
/// The directory the archive unpacks to: its own name without the
|
|
/// `.tar.bz2` — the CDN's archives carry exactly that top-level dir.
|
|
pub fn extract_dir_name(&self) -> String {
|
|
self.name
|
|
.strip_suffix(".tar.bz2")
|
|
.unwrap_or(&self.name)
|
|
.to_string()
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------- the index
|
|
|
|
/// A strict, allocation-light JSON reader over the index text: it walks
|
|
/// the structure it expects and refuses anything else. The index is a
|
|
/// map of platform → object, each with `"versions"`: an array of objects
|
|
/// with string `cef_version`/`channel` and `"files"`: an array of objects
|
|
/// with string `type`/`name`/`sha1` and a number `size`. Other keys are
|
|
/// skipped, whatever their type.
|
|
struct Scanner<'a> {
|
|
s: &'a [u8],
|
|
i: usize,
|
|
}
|
|
|
|
impl<'a> Scanner<'a> {
|
|
fn new(s: &'a str) -> Self {
|
|
Scanner { s: s.as_bytes(), i: 0 }
|
|
}
|
|
|
|
fn err<T>(&self, what: &str) -> Result<T, String> {
|
|
Err(format!("cef index: {what} at byte {}", self.i))
|
|
}
|
|
|
|
fn ws(&mut self) {
|
|
while self.i < self.s.len() && matches!(self.s[self.i], b' ' | b'\n' | b'\r' | b'\t') {
|
|
self.i += 1;
|
|
}
|
|
}
|
|
|
|
fn peek(&mut self) -> Option<u8> {
|
|
self.ws();
|
|
self.s.get(self.i).copied()
|
|
}
|
|
|
|
fn expect(&mut self, c: u8) -> Result<(), String> {
|
|
if self.peek() == Some(c) {
|
|
self.i += 1;
|
|
Ok(())
|
|
} else {
|
|
self.err(&format!("expected `{}`", c as char))
|
|
}
|
|
}
|
|
|
|
fn string(&mut self) -> Result<String, String> {
|
|
self.expect(b'"')?;
|
|
let mut out = String::new();
|
|
loop {
|
|
let Some(&c) = self.s.get(self.i) else { return self.err("unterminated string") };
|
|
self.i += 1;
|
|
match c {
|
|
b'"' => return Ok(out),
|
|
b'\\' => {
|
|
let Some(&e) = self.s.get(self.i) else { return self.err("bad escape") };
|
|
self.i += 1;
|
|
match e {
|
|
b'"' => out.push('"'),
|
|
b'\\' => out.push('\\'),
|
|
b'/' => out.push('/'),
|
|
b'n' => out.push('\n'),
|
|
b't' => out.push('\t'),
|
|
b'r' => out.push('\r'),
|
|
b'b' | b'f' => {}
|
|
b'u' => {
|
|
// The index carries no non-ASCII in the fields we
|
|
// read; keep the four hex digits as text rather
|
|
// than decode them.
|
|
let end = (self.i + 4).min(self.s.len());
|
|
out.push_str(&String::from_utf8_lossy(&self.s[self.i..end]));
|
|
self.i = end;
|
|
}
|
|
_ => return self.err("bad escape"),
|
|
}
|
|
}
|
|
_ => out.push(c as char),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn number(&mut self) -> Result<u64, String> {
|
|
self.ws();
|
|
let start = self.i;
|
|
while self.i < self.s.len() && (self.s[self.i].is_ascii_digit() || matches!(self.s[self.i], b'.' | b'-' | b'+' | b'e' | b'E')) {
|
|
self.i += 1;
|
|
}
|
|
let text = std::str::from_utf8(&self.s[start..self.i]).map_err(|_| "bad number".to_string())?;
|
|
text.parse::<u64>().or_else(|_| text.parse::<f64>().map(|f| f as u64)).map_err(|_| format!("cef index: bad number `{text}`"))
|
|
}
|
|
|
|
/// Skip any value.
|
|
fn skip(&mut self) -> Result<(), String> {
|
|
match self.peek() {
|
|
Some(b'"') => self.string().map(|_| ()),
|
|
Some(b'{') => {
|
|
self.i += 1;
|
|
if self.peek() == Some(b'}') {
|
|
self.i += 1;
|
|
return Ok(());
|
|
}
|
|
loop {
|
|
self.string()?;
|
|
self.expect(b':')?;
|
|
self.skip()?;
|
|
match self.peek() {
|
|
Some(b',') => self.i += 1,
|
|
Some(b'}') => {
|
|
self.i += 1;
|
|
return Ok(());
|
|
}
|
|
_ => return self.err("expected `,` or `}`"),
|
|
}
|
|
}
|
|
}
|
|
Some(b'[') => {
|
|
self.i += 1;
|
|
if self.peek() == Some(b']') {
|
|
self.i += 1;
|
|
return Ok(());
|
|
}
|
|
loop {
|
|
self.skip()?;
|
|
match self.peek() {
|
|
Some(b',') => self.i += 1,
|
|
Some(b']') => {
|
|
self.i += 1;
|
|
return Ok(());
|
|
}
|
|
_ => return self.err("expected `,` or `]`"),
|
|
}
|
|
}
|
|
}
|
|
Some(b't') | Some(b'f') | Some(b'n') | Some(b'-') | Some(b'0'..=b'9') => {
|
|
while self.i < self.s.len() && !matches!(self.s[self.i], b',' | b'}' | b']' | b' ' | b'\n' | b'\r' | b'\t') {
|
|
self.i += 1;
|
|
}
|
|
Ok(())
|
|
}
|
|
_ => self.err("unexpected value"),
|
|
}
|
|
}
|
|
|
|
/// `{ "k": v, ... }` — calls `f(key)` for every key with the scanner
|
|
/// positioned at the value; `f` must consume the value.
|
|
fn object(&mut self, mut f: impl FnMut(&mut Self, &str) -> Result<(), String>) -> Result<(), String> {
|
|
self.expect(b'{')?;
|
|
if self.peek() == Some(b'}') {
|
|
self.i += 1;
|
|
return Ok(());
|
|
}
|
|
loop {
|
|
let key = self.string()?;
|
|
self.expect(b':')?;
|
|
f(self, &key)?;
|
|
match self.peek() {
|
|
Some(b',') => self.i += 1,
|
|
Some(b'}') => {
|
|
self.i += 1;
|
|
return Ok(());
|
|
}
|
|
_ => return self.err("expected `,` or `}`"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// `[ v, ... ]` — calls `f` for every element with the scanner at it.
|
|
fn array(&mut self, mut f: impl FnMut(&mut Self) -> Result<(), String>) -> Result<(), String> {
|
|
self.expect(b'[')?;
|
|
if self.peek() == Some(b']') {
|
|
self.i += 1;
|
|
return Ok(());
|
|
}
|
|
loop {
|
|
f(self)?;
|
|
match self.peek() {
|
|
Some(b',') => self.i += 1,
|
|
Some(b']') => {
|
|
self.i += 1;
|
|
return Ok(());
|
|
}
|
|
_ => return self.err("expected `,` or `]`"),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Every archive the index lists for `platform`, in the index's order
|
|
/// (newest first), any channel, any file type.
|
|
pub fn archives_for(index_json: &str, platform: &str) -> Result<Vec<Archive>, String> {
|
|
if !PLATFORMS.contains(&platform) {
|
|
return Err(format!("cef: `{platform}` is not a platform we link (one of {})", PLATFORMS.join(", ")));
|
|
}
|
|
let mut sc = Scanner::new(index_json);
|
|
let mut found_platform = false;
|
|
let mut out: Vec<Archive> = Vec::new();
|
|
sc.object(|sc, key| {
|
|
if key != platform {
|
|
return sc.skip();
|
|
}
|
|
found_platform = true;
|
|
sc.object(|sc, key| {
|
|
if key != "versions" {
|
|
return sc.skip();
|
|
}
|
|
sc.array(|sc| {
|
|
let mut cef_version = String::new();
|
|
let mut channel = String::from("stable");
|
|
let mut files: Vec<(String, String, String, u64)> = Vec::new();
|
|
sc.object(|sc, key| match key {
|
|
"cef_version" => {
|
|
cef_version = sc.string()?;
|
|
Ok(())
|
|
}
|
|
"channel" => {
|
|
channel = sc.string()?;
|
|
Ok(())
|
|
}
|
|
"files" => sc.array(|sc| {
|
|
let (mut ty, mut name, mut sha1, mut size) = (String::new(), String::new(), String::new(), 0u64);
|
|
sc.object(|sc, key| match key {
|
|
"type" => {
|
|
ty = sc.string()?;
|
|
Ok(())
|
|
}
|
|
"name" => {
|
|
name = sc.string()?;
|
|
Ok(())
|
|
}
|
|
"sha1" => {
|
|
sha1 = sc.string()?;
|
|
Ok(())
|
|
}
|
|
"size" => {
|
|
size = sc.number()?;
|
|
Ok(())
|
|
}
|
|
_ => sc.skip(),
|
|
})?;
|
|
files.push((ty, name, sha1, size));
|
|
Ok(())
|
|
}),
|
|
_ => sc.skip(),
|
|
})?;
|
|
if cef_version.is_empty() {
|
|
return Err("cef index: a version entry without cef_version".into());
|
|
}
|
|
for (ty, name, sha1, size) in files {
|
|
if ty != "standard" {
|
|
continue;
|
|
}
|
|
if name.is_empty() {
|
|
return Err(format!("cef index: {cef_version} has a standard file without a name"));
|
|
}
|
|
out.push(Archive { cef_version: cef_version.clone(), channel: channel.clone(), name, sha1, size });
|
|
}
|
|
Ok(())
|
|
})
|
|
})
|
|
})?;
|
|
if !found_platform {
|
|
return Err(format!("cef index: no `{platform}` section"));
|
|
}
|
|
Ok(out)
|
|
}
|
|
|
|
/// The archive to fetch: the newest stable standard build for `platform`,
|
|
/// or, with a pin, the stable build whose cef_version starts with it
|
|
/// (`138.0.59` matches `138.0.59+g21d63d5+chromium-138.0.7204.306`). Beta
|
|
/// builds are never picked.
|
|
pub fn pick(index_json: &str, platform: &str, pin: Option<&str>) -> Result<Archive, String> {
|
|
let archives = archives_for(index_json, platform)?;
|
|
let stable = archives.into_iter().filter(|a| a.channel == "stable");
|
|
match pin {
|
|
Some(pin) => {
|
|
let pin = pin.trim();
|
|
stable
|
|
.filter(|a| a.cef_version == pin || a.cef_version.starts_with(&format!("{pin}+")))
|
|
.next()
|
|
.ok_or_else(|| format!("cef index: no stable standard build {pin} for {platform}"))
|
|
}
|
|
None => stable
|
|
.max_by(|a, b| version_key(&a.cef_version).cmp(&version_key(&b.cef_version)))
|
|
.ok_or_else(|| format!("cef index: no stable standard build for {platform}")),
|
|
}
|
|
}
|
|
|
|
/// `138.0.59+g21d63d5+chromium-138.0.7204.306` → (138, 0, 59): the CDN's
|
|
/// order is newest first, but the pick must not depend on it.
|
|
fn version_key(v: &str) -> (u64, u64, u64) {
|
|
let head = v.split('+').next().unwrap_or(v);
|
|
let mut it = head.split('.').map(|p| p.parse::<u64>().unwrap_or(0));
|
|
(it.next().unwrap_or(0), it.next().unwrap_or(0), it.next().unwrap_or(0))
|
|
}
|
|
|
|
// ------------------------------------------------------------ the pointer
|
|
|
|
/// `current-<platform>` as a text file: the extracted directory's name
|
|
/// (relative to the prebuilt dir) on the first line. Works on every
|
|
/// filesystem without privileges; the older symlink form is still read.
|
|
pub struct Pointer;
|
|
|
|
impl Pointer {
|
|
pub fn file(prebuilt_dir: &Path, platform: &str) -> PathBuf {
|
|
prebuilt_dir.join(format!("current-{platform}.txt"))
|
|
}
|
|
|
|
pub fn link(prebuilt_dir: &Path, platform: &str) -> PathBuf {
|
|
prebuilt_dir.join(format!("current-{platform}"))
|
|
}
|
|
|
|
/// The dist dir the pointer names, when it exists: the text file
|
|
/// first, then the symlink (or a real directory of that name).
|
|
pub fn read(prebuilt_dir: &Path, platform: &str) -> Option<PathBuf> {
|
|
let file = Self::file(prebuilt_dir, platform);
|
|
if let Ok(text) = fs::read_to_string(&file) {
|
|
let name = text.lines().next().unwrap_or("").trim();
|
|
if !name.is_empty() {
|
|
let dir = prebuilt_dir.join(name);
|
|
if dir.join("include").is_dir() {
|
|
return Some(dir);
|
|
}
|
|
}
|
|
}
|
|
let link = Self::link(prebuilt_dir, platform);
|
|
if link.join("include").is_dir() {
|
|
return Some(link);
|
|
}
|
|
None
|
|
}
|
|
|
|
pub fn write(prebuilt_dir: &Path, platform: &str, dir_name: &str) -> Result<(), String> {
|
|
let file = Self::file(prebuilt_dir, platform);
|
|
fs::write(&file, format!("{dir_name}\n")).map_err(|e| format!("cef: cannot write {}: {e}", file.display()))
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------- the fetch
|
|
|
|
/// Progress goes to cargo as warnings: a build that sits for minutes on a
|
|
/// 300 MB download must say so.
|
|
fn say(msg: &str) {
|
|
println!("cargo:warning=cef: {msg}");
|
|
}
|
|
|
|
fn http_get(url: &str) -> Result<ureq::Body, String> {
|
|
let response = ureq::get(url).call().map_err(|e| format!("cef: GET {url}: {e}"))?;
|
|
Ok(response.into_body())
|
|
}
|
|
|
|
pub fn fetch_index() -> Result<String, String> {
|
|
let mut body = http_get(INDEX_URL)?;
|
|
let text = body.read_to_string().map_err(|e| format!("cef: reading {INDEX_URL}: {e}"))?;
|
|
Ok(text)
|
|
}
|
|
|
|
fn sha1_hex(path: &Path) -> Result<String, String> {
|
|
let mut file = fs::File::open(path).map_err(|e| format!("cef: open {}: {e}", path.display()))?;
|
|
let mut hasher = sha1_smol::Sha1::new();
|
|
let mut buf = vec![0u8; 1 << 20];
|
|
loop {
|
|
let n = file.read(&mut buf).map_err(|e| format!("cef: read {}: {e}", path.display()))?;
|
|
if n == 0 {
|
|
break;
|
|
}
|
|
hasher.update(&buf[..n]);
|
|
}
|
|
Ok(hasher.digest().to_string())
|
|
}
|
|
|
|
/// Download `archive` into `prebuilt_dir` (to a `.part` file, renamed when
|
|
/// complete), unless the archive is already there and matches its sha1.
|
|
fn download(archive: &Archive, prebuilt_dir: &Path) -> Result<PathBuf, String> {
|
|
let path = prebuilt_dir.join(&archive.name);
|
|
if path.is_file() {
|
|
if archive.sha1.is_empty() || sha1_hex(&path)? == archive.sha1 {
|
|
say(&format!("using the archive already at {}", path.display()));
|
|
return Ok(path);
|
|
}
|
|
say(&format!("{} does not match the index's sha1; downloading again", path.display()));
|
|
let _ = fs::remove_file(&path);
|
|
}
|
|
let part = prebuilt_dir.join(format!("{}.part", archive.name));
|
|
let _ = fs::remove_file(&part);
|
|
say(&format!(
|
|
"downloading {} ({:.0} MB) — this takes a few minutes",
|
|
archive.url(),
|
|
archive.size as f64 / 1e6
|
|
));
|
|
let url = archive.url();
|
|
let mut body = http_get(&url)?;
|
|
let mut reader = body.as_reader();
|
|
let mut out = fs::File::create(&part).map_err(|e| format!("cef: create {}: {e}", part.display()))?;
|
|
let mut buf = vec![0u8; 1 << 20];
|
|
let mut done: u64 = 0;
|
|
let mut next_report: u64 = 50_000_000;
|
|
loop {
|
|
let n = reader.read(&mut buf).map_err(|e| format!("cef: download {url}: {e}"))?;
|
|
if n == 0 {
|
|
break;
|
|
}
|
|
out.write_all(&buf[..n]).map_err(|e| format!("cef: write {}: {e}", part.display()))?;
|
|
done += n as u64;
|
|
if done >= next_report {
|
|
say(&format!("{:.0} of {:.0} MB", done as f64 / 1e6, archive.size as f64 / 1e6));
|
|
next_report += 50_000_000;
|
|
}
|
|
}
|
|
drop(out);
|
|
if archive.size > 0 && done != archive.size {
|
|
return Err(format!("cef: {} is {done} bytes; the index says {}", archive.name, archive.size));
|
|
}
|
|
if !archive.sha1.is_empty() {
|
|
let actual = sha1_hex(&part)?;
|
|
if actual != archive.sha1 {
|
|
let _ = fs::remove_file(&part);
|
|
return Err(format!("cef: sha1 of {} is {actual}; the index says {}", archive.name, archive.sha1));
|
|
}
|
|
say("sha1 verified");
|
|
}
|
|
fs::rename(&part, &path).map_err(|e| format!("cef: rename {}: {e}", part.display()))?;
|
|
Ok(path)
|
|
}
|
|
|
|
/// Unpack the archive beside itself; the top-level directory inside the
|
|
/// tarball is the dist dir. A half-extracted directory from an earlier
|
|
/// failure is removed first.
|
|
fn extract(archive_path: &Path, prebuilt_dir: &Path, dir_name: &str) -> Result<PathBuf, String> {
|
|
let dir = prebuilt_dir.join(dir_name);
|
|
if dir.exists() {
|
|
say(&format!("removing the incomplete {}", dir.display()));
|
|
fs::remove_dir_all(&dir).map_err(|e| format!("cef: remove {}: {e}", dir.display()))?;
|
|
}
|
|
say(&format!("extracting {} into {}", archive_path.display(), prebuilt_dir.display()));
|
|
let file = fs::File::open(archive_path).map_err(|e| format!("cef: open {}: {e}", archive_path.display()))?;
|
|
let decoder = bzip2::read::MultiBzDecoder::new(std::io::BufReader::new(file));
|
|
let mut tar = tar::Archive::new(decoder);
|
|
tar.set_preserve_permissions(true);
|
|
tar.unpack(prebuilt_dir).map_err(|e| format!("cef: extract {}: {e}", archive_path.display()))?;
|
|
if !dir.join("include").is_dir() {
|
|
return Err(format!("cef: {} unpacked but has no include/ — not a CEF distribution", dir.display()));
|
|
}
|
|
Ok(dir)
|
|
}
|
|
|
|
/// What the build wants, in one shape for both the real run and the dry run.
|
|
#[derive(Clone, Debug)]
|
|
pub struct Plan {
|
|
pub platform: String,
|
|
pub archive: Archive,
|
|
pub extract_dir: PathBuf,
|
|
pub archive_path: PathBuf,
|
|
}
|
|
|
|
impl Plan {
|
|
pub fn describe(&self) -> String {
|
|
format!(
|
|
"platform {}: {} ({}), {} bytes, sha1 {}\n from {}\n archive {}\n extract to {}",
|
|
self.platform,
|
|
self.archive.cef_version,
|
|
self.archive.channel,
|
|
self.archive.size,
|
|
self.archive.sha1,
|
|
self.archive.url(),
|
|
self.archive_path.display(),
|
|
self.extract_dir.display()
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Resolve the index into a plan for `platform` — the dry run's whole job.
|
|
pub fn plan(prebuilt_dir: &Path, platform: &str, pin: Option<&str>) -> Result<Plan, String> {
|
|
let index = fetch_index()?;
|
|
let archive = pick(&index, platform, pin)?;
|
|
let dir_name = archive.extract_dir_name();
|
|
Ok(Plan {
|
|
platform: platform.to_string(),
|
|
extract_dir: prebuilt_dir.join(&dir_name),
|
|
archive_path: prebuilt_dir.join(&archive.name),
|
|
archive,
|
|
})
|
|
}
|
|
|
|
/// The dist dir for `platform` under `prebuilt_dir`: the one the pointer
|
|
/// names, else downloaded, verified, extracted and pointed at. `offline`
|
|
/// turns a miss into an error naming what to put where.
|
|
pub fn ensure_dist(prebuilt_dir: &Path, platform: &str, pin: Option<&str>, offline: bool) -> Result<PathBuf, String> {
|
|
if let Some(dir) = Pointer::read(prebuilt_dir, platform) {
|
|
return Ok(dir);
|
|
}
|
|
if offline {
|
|
return Err(format!(
|
|
"cef: no distribution for {platform} under {} and MAKEPAD_CEF_OFFLINE is set — put a cef_binary_*_{platform} directory there and name it in {}",
|
|
prebuilt_dir.display(),
|
|
Pointer::file(prebuilt_dir, platform).display()
|
|
));
|
|
}
|
|
fs::create_dir_all(prebuilt_dir).map_err(|e| format!("cef: create {}: {e}", prebuilt_dir.display()))?;
|
|
let plan = plan(prebuilt_dir, platform, pin)?;
|
|
say(&format!("no distribution for {platform}; {}", plan.describe().replace('\n', " ")));
|
|
let dir_name = plan.archive.extract_dir_name();
|
|
let dir = if plan.extract_dir.join("include").is_dir() {
|
|
say(&format!("using the extracted {}", plan.extract_dir.display()));
|
|
plan.extract_dir.clone()
|
|
} else {
|
|
let archive_path = download(&plan.archive, prebuilt_dir)?;
|
|
extract(&archive_path, prebuilt_dir, &dir_name)?
|
|
};
|
|
Pointer::write(prebuilt_dir, platform, &dir_name)?;
|
|
say(&format!("{platform} → {} ({})", dir_name, plan.archive.cef_version));
|
|
Ok(dir)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
const INDEX: &str = r#"{
|
|
"linux64": {"versions": [
|
|
{"cef_version": "144.0.34+g8fc21c8+chromium-144.0.7559.261", "chromium_version": "144.0.7559.261", "channel": "stable",
|
|
"files": [{"type": "standard", "name": "cef_binary_144.0.34+g8fc21c8+chromium-144.0.7559.261_linux64.tar.bz2", "sha1": "23e2", "size": 872549074, "last_modified": "x"},
|
|
{"type": "minimal", "name": "cef_binary_144.0.34+g8fc21c8+chromium-144.0.7559.261_linux64_minimal.tar.bz2", "sha1": "aa", "size": 1}]},
|
|
{"cef_version": "145.0.1+gbeta+chromium-145.0.1.1", "channel": "beta",
|
|
"files": [{"type": "standard", "name": "cef_binary_145.0.1+gbeta+chromium-145.0.1.1_linux64.tar.bz2", "sha1": "bb", "size": 2}]},
|
|
{"cef_version": "144.0.32+g5ce7d26+chromium-144.0.7559.258", "channel": "stable",
|
|
"files": [{"type": "standard", "name": "cef_binary_144.0.32+g5ce7d26+chromium-144.0.7559.258_linux64.tar.bz2", "sha1": "0827", "size": 872549258}]},
|
|
{"cef_version": "138.0.59+g21d63d5+chromium-138.0.7204.306", "channel": "stable",
|
|
"files": [{"type": "standard", "name": "cef_binary_138.0.59+g21d63d5+chromium-138.0.7204.306_linux64.tar.bz2", "sha1": "913e", "size": 810258845}]}
|
|
]},
|
|
"linux32": {"versions": []},
|
|
"windows64": {"versions": [
|
|
{"cef_version": "144.0.34+g8fc21c8+chromium-144.0.7559.261", "channel": "stable",
|
|
"files": [{"type": "standard", "name": "cef_binary_144.0.34+g8fc21c8+chromium-144.0.7559.261_windows64.tar.bz2", "sha1": "81e9", "size": 320482619}]}
|
|
]}
|
|
}"#;
|
|
|
|
#[test]
|
|
fn the_newest_stable_standard_build_wins_and_beta_is_never_picked() {
|
|
let a = pick(INDEX, "linux64", None).unwrap();
|
|
assert_eq!(a.cef_version, "144.0.34+g8fc21c8+chromium-144.0.7559.261");
|
|
assert_eq!(a.channel, "stable");
|
|
assert_eq!(a.sha1, "23e2");
|
|
assert_eq!(a.size, 872549074);
|
|
assert_eq!(a.url(), "https://cef-builds.spotifycdn.com/cef_binary_144.0.34+g8fc21c8+chromium-144.0.7559.261_linux64.tar.bz2");
|
|
assert_eq!(a.extract_dir_name(), "cef_binary_144.0.34+g8fc21c8+chromium-144.0.7559.261_linux64");
|
|
// Minimal archives are never listed; the beta 145 is skipped even
|
|
// though it is newer.
|
|
let all = archives_for(INDEX, "linux64").unwrap();
|
|
assert_eq!(all.len(), 4);
|
|
assert!(all.iter().all(|a| !a.name.contains("minimal")));
|
|
}
|
|
|
|
#[test]
|
|
fn a_pin_is_honoured_by_prefix_or_whole() {
|
|
assert_eq!(pick(INDEX, "linux64", Some("138.0.59")).unwrap().sha1, "913e");
|
|
assert_eq!(pick(INDEX, "linux64", Some("144.0.32+g5ce7d26+chromium-144.0.7559.258")).unwrap().sha1, "0827");
|
|
// A pin on the beta is refused: only stable builds are ever picked.
|
|
assert!(pick(INDEX, "linux64", Some("145.0.1")).unwrap_err().contains("no stable standard build 145.0.1"));
|
|
assert!(pick(INDEX, "linux64", Some("999")).unwrap_err().contains("999"));
|
|
}
|
|
|
|
#[test]
|
|
fn a_missing_or_unsupported_platform_is_a_clear_error() {
|
|
assert!(pick(INDEX, "macosarm64", None).unwrap_err().contains("no `macosarm64` section"));
|
|
assert!(pick(INDEX, "linux32", None).unwrap_err().contains("not a platform we link"));
|
|
assert!(pick("{\"linux64\": {\"versions\": [{\"channel\": \"stable\", \"files\": []}]}}", "linux64", None)
|
|
.unwrap_err()
|
|
.contains("without cef_version"));
|
|
assert!(pick("{\"linux64\": ", "linux64", None).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn the_pointer_file_names_the_dist_and_survives_a_missing_dir() {
|
|
let root = std::env::temp_dir().join(format!("cef-pointer-test-{}", std::process::id()));
|
|
let _ = fs::remove_dir_all(&root);
|
|
fs::create_dir_all(root.join("cef_binary_1_linux64").join("include")).unwrap();
|
|
assert!(Pointer::read(&root, "linux64").is_none());
|
|
Pointer::write(&root, "linux64", "cef_binary_1_linux64").unwrap();
|
|
assert_eq!(Pointer::read(&root, "linux64").unwrap(), root.join("cef_binary_1_linux64"));
|
|
// A pointer at a directory that is not a dist is ignored.
|
|
Pointer::write(&root, "linux64", "nowhere").unwrap();
|
|
assert!(Pointer::read(&root, "linux64").is_none());
|
|
// The older symlink form still counts.
|
|
#[cfg(unix)]
|
|
{
|
|
std::os::unix::fs::symlink(root.join("cef_binary_1_linux64"), Pointer::link(&root, "linux64")).unwrap();
|
|
assert_eq!(Pointer::read(&root, "linux64").unwrap(), Pointer::link(&root, "linux64"));
|
|
}
|
|
let _ = fs::remove_dir_all(&root);
|
|
}
|
|
|
|
#[test]
|
|
fn version_keys_order_numerically() {
|
|
assert!(version_key("144.0.34+g8fc21c8+chromium-144.0.7559.261") > version_key("144.0.32+g5ce7d26+chromium-144.0.7559.258"));
|
|
assert!(version_key("144.0.32+x") > version_key("138.0.59+x"));
|
|
assert!(version_key("10.0.0") > version_key("9.9.9"));
|
|
}
|
|
}
|