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
10 changes: 6 additions & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
44 changes: 40 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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`)
Expand Down
4 changes: 4 additions & 0 deletions src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
}
Expand Down
4 changes: 2 additions & 2 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
128 changes: 72 additions & 56 deletions src/commands/emulator.rs
Original file line number Diff line number Diff line change
@@ -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<bool> {
// 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<String> {
#[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<String> {
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())
}
49 changes: 45 additions & 4 deletions src/commands/postinstall.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}

Expand Down Expand Up @@ -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(())
}

Expand All @@ -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");
Expand All @@ -62,6 +88,14 @@ fn create_alias_unix(dir_path: &Path) -> Result<()> {
fn find_writable_path() -> Option<PathBuf> {
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 {
Expand Down Expand Up @@ -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::*;
Expand Down
Loading