diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index eb32953..a7e5e28 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -45,15 +45,17 @@ jobs: job: # https://github.com/actions/runner-images # Linux + - { target: aarch64-unknown-linux-musl, os: ubuntu-24.04 } - { target: arm-unknown-linux-musleabihf, os: ubuntu-24.04 } - { target: x86_64-unknown-linux-musl, os: ubuntu-24.04 } - { target: i686-unknown-linux-musl, os: ubuntu-24.04 } # OS X - - { target: aarch64-apple-darwin, os: macos-14 } + - { target: aarch64-apple-darwin, os: macos-15 } + - { target: x86_64-apple-darwin, os: macos-15-intel } # Windows - - { target: i686-pc-windows-msvc, os: windows-2022 } - - { target: x86_64-pc-windows-gnu, os: windows-2022 } - - { target: x86_64-pc-windows-msvc, os: windows-2022 } + - { target: aarch64-pc-windows-msvc, os: windows-11-arm } + - { target: x86_64-pc-windows-msvc, os: windows-2025 } + - { target: i686-pc-windows-msvc, os: windows-2025 } env: BUILD_CMD: cargo steps: diff --git a/Cargo.lock b/Cargo.lock index 4bb050b..f44c7e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -498,6 +498,18 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "filetime" +version = "0.2.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.60.2", +] + [[package]] name = "firefly-types" version = "0.7.1" @@ -521,6 +533,7 @@ dependencies = [ "data-encoding", "directories", "firefly-types", + "flate2", "hound", "image", "libflate", @@ -531,6 +544,7 @@ dependencies = [ "serde_json", "serialport", "sha2", + "tar", "toml", "ureq", "wasm-encoder", @@ -540,9 +554,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.35" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" dependencies = [ "crc32fast", "miniz_oxide", @@ -809,6 +823,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags 2.9.0", "libc", + "redox_syscall", ] [[package]] @@ -882,9 +897,9 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "miniz_oxide" -version = "0.8.3" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", "simd-adler32", @@ -1435,6 +1450,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tar" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -1935,6 +1961,16 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix 1.0.7", +] + [[package]] name = "zerocopy" version = "0.7.35" diff --git a/Cargo.toml b/Cargo.toml index 85a0622..1495e9d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,8 @@ data-encoding = "2.9.0" directories = "6.0.0" # Serialize app config into meta file in the ROM firefly-types = { version = "0.7.1" } +# Read gz archives (to install emulator). +flate2 = "1.1.5" # Decode wav files hound = "3.5.1" # Parse PNG images @@ -58,6 +60,8 @@ serde_json = "1.0.149" serialport = "4.8.1" # Calculate file checksum sha2 = "0.10.9" +# Read gz archives (to install emulator). +tar = "0.4.44" # Deserialize firefly.toml toml = "0.9.8" # Download remote files (`url` field in `firefly.toml`) diff --git a/src/args.rs b/src/args.rs index 40ca40b..ca9f734 100644 --- a/src/args.rs +++ b/src/args.rs @@ -216,6 +216,10 @@ pub struct NewArgs { #[derive(Debug, Parser)] pub struct EmulatorArgs { + /// Download the latest emulator release. + #[arg(long, default_value_t = false)] + pub update: bool, + /// Arguments to pass into the emulator. pub args: Vec, } diff --git a/src/cli.rs b/src/cli.rs index 0bf4dc6..d587707 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -6,13 +6,13 @@ use std::path::PathBuf; pub fn run_command(vfs: PathBuf, command: &Commands) -> anyhow::Result<()> { use Commands::*; match command { - Postinstall => cmd_postinstall(), + Postinstall => cmd_postinstall(&vfs), Build(args) => cmd_build(vfs, args), Export(args) => cmd_export(&vfs, args), Import(args) => cmd_import(&vfs, args), New(args) => cmd_new(args), Test(args) => cmd_test(args), - Emulator(args) => cmd_emulator(args), + Emulator(args) => cmd_emulator(&vfs, args), Badges(args) => cmd_badges(&vfs, args), Boards(args) => cmd_boards(&vfs, args), Inspect(args) => cmd_inspect(&vfs, args), diff --git a/src/commands/emulator.rs b/src/commands/emulator.rs index e9f2bc4..e22f536 100644 --- a/src/commands/emulator.rs +++ b/src/commands/emulator.rs @@ -1,70 +1,86 @@ use crate::args::EmulatorArgs; use crate::langs::check_output; use anyhow::{Context, Result, bail}; +use flate2::read::GzDecoder; +use std::fs::File; +use std::path::Path; use std::process::Command; +use tar::Archive; -pub fn cmd_emulator(args: &EmulatorArgs) -> Result<()> { - let executed_dev = run_dev(args)?; - if executed_dev { - return Ok(()); - } - let bins = [ - "./firefly_emulator", - "./firefly_emulator.exe", - "firefly_emulator", - "firefly_emulator.exe", - "firefly-emulator", - "firefly-emulator.exe", - ]; - for bin in bins { - if binary_exists(bin) { - println!("running {bin}..."); - let output = Command::new(bin).args(&args.args).output()?; - check_output(&output).context("run emulator")?; - return Ok(()); - } +pub fn cmd_emulator(vfs: &Path, args: &EmulatorArgs) -> Result<()> { + #[cfg(target_os = "windows")] + const BINARY_NAME: &str = "firefly-emulator.exe"; + #[cfg(not(target_os = "windows"))] + const BINARY_NAME: &str = "firefly-emulator"; + + // TODO(@orsinium): always use the global vfs. + let bin_path = vfs.join(BINARY_NAME); + if args.update || !bin_path.exists() { + println!("⏳️ downloading emulator..."); + download_emulator(&bin_path).context("download emulator")?; } - bail!("emulator not installed"); + println!("⌛ running..."); + let output = Command::new(bin_path).args(&args.args).output()?; + check_output(&output).context("run emulator")?; + Ok(()) } -fn run_dev(args: &EmulatorArgs) -> Result { - // Check common places where firefly repo might be clonned. - // If found, run the dev version using cargo. - let Some(base_dirs) = directories::BaseDirs::new() else { - return Ok(false); - }; - if !binary_exists("cargo") { - return Ok(false); +fn download_emulator(bin_path: &Path) -> Result<()> { + // Send HTTP request. + let url = get_release_url().context("get latest release URL")?; + let resp = ureq::get(&url).call().context("send HTTP request")?; + let body = resp.into_body().into_reader(); + + // Extract archive. + if url.ends_with(".tar.gz") || url.ends_with(".tgz") { + let tar = GzDecoder::new(body); + let mut archive = Archive::new(tar); + let vfs = bin_path.parent().unwrap(); + archive.unpack(vfs).context("extract binary")?; + } else { + bail!("unsupported archive format") } - let home = base_dirs.home_dir(); - let paths = [ - home.join("Documents").join("firefly"), - home.join("ff").join("firefly"), - home.join("github").join("firefly"), - home.join("firefly"), - ]; - for dir_path in paths { - let cargo_path = dir_path.join("Cargo.toml"); - if !cargo_path.is_file() { - continue; - } - println!("running dev version from {}...", dir_path.to_str().unwrap()); - let output = Command::new("cargo") - .arg("run") - .arg("--") - .args(&args.args) - .current_dir(dir_path) - .output()?; - check_output(&output).context("run emulator")?; - return Ok(true); + + // chmod. + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let file = File::open(bin_path).context("open file")?; + let mut perm = file.metadata().context("read file meta")?.permissions(); + perm.set_mode(0o700); + std::fs::set_permissions(bin_path, perm).context("set permissions")?; } - Ok(false) + + Ok(()) } -fn binary_exists(bin: &str) -> bool { - let output = Command::new(bin).arg("--help").output(); - let Ok(output) = output else { - return false; +fn get_release_url() -> Result { + #[cfg(target_os = "windows")] + const SUFFIX: &str = "x86_64-pc-windows-msvc.tgz"; + #[cfg(target_os = "macos")] + const SUFFIX: &str = "aarch64-apple-darwin.tgz"; + #[cfg(target_os = "linux")] + const SUFFIX: &str = "x86_64-unknown-linux-gnu.tgz"; + + let version = get_latest_version()?; + let repo = "https://github.com/firefly-zero/firefly-emulator-bin"; + Ok(format!( + "{repo}/releases/latest/download/firefly-emulator-v{version}-{SUFFIX}" + )) +} + +fn get_latest_version() -> Result { + let url = "https://github.com/firefly-zero/firefly-emulator-bin/releases/latest"; + let req = ureq::get(url); + let req = req.config().max_redirects(0).build(); + let resp = req.call()?; + if resp.status() != 302 { + bail!("unexpected status code: {}", resp.status()); + } + let Some(loc) = resp.headers().get("Location") else { + bail!("no redirect Location found in response"); }; - output.status.success() + let loc = loc.to_str()?; + let version = loc.split('/').next_back().unwrap(); + Ok(version.to_owned()) } diff --git a/src/commands/postinstall.rs b/src/commands/postinstall.rs index f6129cb..4fe42d7 100644 --- a/src/commands/postinstall.rs +++ b/src/commands/postinstall.rs @@ -4,9 +4,18 @@ use std::{ path::{Path, PathBuf}, }; -pub fn cmd_postinstall() -> Result<()> { - let path = move_self()?; - create_alias(&path)?; +pub fn cmd_postinstall(vfs: &Path) -> Result<()> { + let path = move_self().context("move binary")?; + delete_alias().context("delete old alias")?; + create_alias(&path).context("create alias")?; + + // While CLI is released (and so is supposed to be updated) + // more often than emulator, most people update CLI only when + // we announce "the big release" which includes a new emulator. + // So, it's safe to assume that when we update CLI, + // emulator also needs to be updated (most of the time). + remove_emulator(vfs); + Ok(()) } @@ -37,7 +46,13 @@ fn move_self_to(new_path: &Path) -> Result<()> { bail!("the binary is execute not by its path"); } let new_path = new_path.join("firefly_cli"); - std::fs::rename(old_path, new_path).context("move binary")?; + + let res = std::fs::rename(&old_path, &new_path); + // Rename fails if src and dst on different mount points. + if res.is_err() { + std::fs::copy(&old_path, &new_path).context("copy binary")?; + _ = std::fs::remove_file(&old_path); + } Ok(()) } @@ -50,6 +65,17 @@ fn create_alias(dir_path: &Path) -> Result<()> { Ok(()) } +fn delete_alias() -> Result<()> { + let paths = load_paths(); + for path in &paths { + let bin_path = path.join("ff"); + if bin_path.is_file() { + std::fs::remove_file(bin_path)?; + } + } + Ok(()) +} + #[cfg(unix)] fn create_alias_unix(dir_path: &Path) -> Result<()> { let old_path = dir_path.join("firefly_cli"); @@ -62,6 +88,14 @@ fn create_alias_unix(dir_path: &Path) -> Result<()> { fn find_writable_path() -> Option { let paths = load_paths(); + // Find a path that already has the old firefly_cli binary. + for path in &paths { + let bin_path = path.join("firefly_cli"); + if bin_path.is_file() { + return Some(path.clone()); + } + } + // Prefer writable paths in the user home directory. if let Some(home) = std::env::home_dir() { for path in &paths { @@ -147,6 +181,13 @@ fn add_path_to(profile: &Path, path: &Path) -> Result<()> { Ok(()) } +fn remove_emulator(vfs: &Path) { + let path = vfs.join("firefly-emulator"); + _ = std::fs::remove_file(path); + let path = vfs.join("firefly-emulator.exe"); + _ = std::fs::remove_file(path); +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/repl_helper.rs b/src/repl_helper.rs index 43a4259..0bcf9e7 100644 --- a/src/repl_helper.rs +++ b/src/repl_helper.rs @@ -16,13 +16,36 @@ impl Helper { let mut hints = Vec::new(); let cmds = [ // commands - "build", "export", "import", "vfs", "cheat", "monitor", "catalog", - // + "build", + "export", + "import", + "new", + "emulator", + "test", + "badges", + "boards", + "vfs", + "runtime", + "inspect", + "shots", + "name", + "catalog", // subcommands - "new", "add", "pub", "priv", "rm", "list", "show", - // + "cheat", + "monitor", + "logs", + "screenshot", + "launch", + "restart", + "exit", + "id", // aliases - "install", "generate", "remove", "app", "author", "ls", + "install", + "generate", + "remove", + "app", + "author", + "ls", ]; for cmd in cmds { let h = CommandHint(cmd.to_string());