diff --git a/README.md b/README.md index b4923a3..a042b96 100644 --- a/README.md +++ b/README.md @@ -145,6 +145,54 @@ cargo run -p czdev --release -- deploy \ --deb path/to/my_app_arm64.deb ``` +### 8. Publishing to the AppStore + +```bash +# Install czdev (one-time, or build from source: cargo build --release -p czdev) +curl -fsSL https://github.com/m5stack/CardputerZero-AppBuilder/releases/latest/download/czdev-macos-aarch64 -o czdev +chmod +x czdev && sudo mv czdev /usr/local/bin/ + +# Login to GitHub (one-time) +czdev login + +# Auto-bump patch version and publish +czdev bump --deb build/my_app_1.0.0_arm64.deb # → my_app_1.0.1_arm64.deb +czdev publish --deb build/my_app_1.0.1_arm64.deb + +# Or publish directly (version in deb must be newer than existing) +czdev publish --deb build/my_app_2.0.0_arm64.deb + +# Remove your own package +czdev unpublish my_app --version 1.0.1 +``` + +## Install czdev + +**Option A — Download prebuilt binary (recommended):** + +| Platform | Command | +|----------|---------| +| macOS Apple Silicon | `curl -fsSL https://github.com/m5stack/CardputerZero-AppBuilder/releases/latest/download/czdev-macos-aarch64 -o czdev && chmod +x czdev && sudo mv czdev /usr/local/bin/` | +| macOS Intel | `curl -fsSL .../czdev-macos-x86_64 -o czdev && chmod +x czdev && sudo mv czdev /usr/local/bin/` | +| Linux x86_64 | `curl -fsSL .../czdev-linux-x86_64 -o czdev && chmod +x czdev && sudo mv czdev /usr/local/bin/` | +| Linux aarch64 | `curl -fsSL .../czdev-linux-aarch64 -o czdev && chmod +x czdev && sudo mv czdev /usr/local/bin/` | +| Windows | Download `czdev-windows-x86_64.exe` from [Releases](https://github.com/m5stack/CardputerZero-AppBuilder/releases) | + +**Option B — Build from source:** + +```bash +git clone --recursive git@github.com:m5stack/CardputerZero-AppBuilder.git +cd CardputerZero-AppBuilder +cargo build --release -p czdev +# Binary at: target/release/czdev +``` + +**Option C — cargo install:** + +```bash +cargo install --git https://github.com/m5stack/CardputerZero-AppBuilder czdev +``` + ## CI Online Build 1. Go to **Actions** > **Build DEB Package** > **Run workflow** diff --git a/crates/czdev/src/bump.rs b/crates/czdev/src/bump.rs new file mode 100644 index 0000000..fa8c303 --- /dev/null +++ b/crates/czdev/src/bump.rs @@ -0,0 +1,129 @@ +use anyhow::{anyhow, Context, Result}; +use std::path::{Path, PathBuf}; +use std::process::Command; + +const PACKAGES_INDEX_URL: &str = + "https://cardputerzero.github.io/packages/dists/stable/main/binary-arm64/Packages"; + +pub fn run(deb: Option<&Path>) -> Result<()> { + let deb_path = resolve_deb(deb)?; + + let package = dpkg_field(&deb_path, "Package")?; + let current_ver = dpkg_field(&deb_path, "Version")?; + + println!("Package: {package}"); + println!("Current version in deb: {current_ver}"); + + // Fetch latest published version + let latest = fetch_latest_version(&package)?; + let next = match &latest { + Some(v) => { + println!("Latest published version: {v}"); + bump_patch(v) + } + None => { + println!("No published version found (new package)"); + bump_patch(¤t_ver) + } + }; + + println!(); + println!("Next version: {next}"); + println!(); + println!("To rebuild with this version, update your package control file:"); + println!(" Version: {next}"); + println!(); + println!("Or if using CMake/packaging scripts, set:"); + println!(" PKG_VERSION={next}"); + + Ok(()) +} + +fn resolve_deb(deb: Option<&Path>) -> Result { + if let Some(p) = deb { + if !p.is_file() { + return Err(anyhow!("file not found: {}", p.display())); + } + return Ok(p.to_path_buf()); + } + let build_dir = Path::new("build"); + if build_dir.is_dir() { + let mut debs: Vec<_> = std::fs::read_dir(build_dir)? + .filter_map(|e| e.ok()) + .filter(|e| e.path().extension().map(|x| x == "deb").unwrap_or(false)) + .collect(); + if debs.len() == 1 { + return Ok(debs.remove(0).path()); + } + if debs.len() > 1 { + return Err(anyhow!( + "multiple .deb files in build/. Specify one with --deb " + )); + } + } + Err(anyhow!("no .deb file found. Specify with --deb ")) +} + +fn dpkg_field(deb: &Path, field: &str) -> Result { + let output = Command::new("dpkg-deb") + .args(["-f", &deb.to_string_lossy(), field]) + .output() + .with_context(|| format!("dpkg-deb -f {field}"))?; + Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) +} + +fn fetch_latest_version(package: &str) -> Result> { + let resp = reqwest::blocking::get(PACKAGES_INDEX_URL); + let content = match resp { + Ok(r) if r.status().is_success() => r.text().unwrap_or_default(), + _ => return Ok(None), + }; + + let mut in_package = false; + let mut latest: Option = None; + + for line in content.lines() { + if line.starts_with("Package: ") { + in_package = line.trim_start_matches("Package: ") == package; + } + if in_package && line.starts_with("Version: ") { + let ver = line.trim_start_matches("Version: ").to_string(); + match &latest { + Some(existing) if compare_versions(&ver, existing) == std::cmp::Ordering::Greater => { + latest = Some(ver); + } + None => latest = Some(ver), + _ => {} + } + } + if line.is_empty() { + in_package = false; + } + } + Ok(latest) +} + +fn bump_patch(version: &str) -> String { + // Handle versions like "1.0.0", "1.0", "0.2.0-1~lofibox23" + // Strip debian revision (everything after first '-') + let base = version.split('-').next().unwrap_or(version); + let parts: Vec<&str> = base.split('.').collect(); + + match parts.len() { + 1 => format!("{}.0.1", parts[0]), + 2 => format!("{}.{}.1", parts[0], parts[1]), + _ => { + let patch: u64 = parts[2].parse().unwrap_or(0); + format!("{}.{}.{}", parts[0], parts[1], patch + 1) + } + } +} + +fn compare_versions(a: &str, b: &str) -> std::cmp::Ordering { + let parse = |v: &str| -> Vec { + v.split(|c: char| c == '.' || c == '-' || c == '~') + .filter_map(|s| s.parse::().ok()) + .collect() + }; + parse(a).cmp(&parse(b)) +} diff --git a/crates/czdev/src/main.rs b/crates/czdev/src/main.rs index 92c57ef..e4c1409 100644 --- a/crates/czdev/src/main.rs +++ b/crates/czdev/src/main.rs @@ -1,5 +1,6 @@ mod auth; mod build; +mod bump; mod deploy; mod doctor; mod github; @@ -73,6 +74,13 @@ enum Command { /// Remove stored GitHub credentials. Logout, + /// Show next version (patch bump) for a package based on published versions. + Bump { + /// Path to the .deb file. If omitted, searches ./build/*.deb + #[arg(long)] + deb: Option, + }, + /// Publish a .deb package to the CardputerZero app store. Publish { /// Path to the .deb file. If omitted, searches ./build/*.deb @@ -107,6 +115,7 @@ fn main() -> Result<()> { Command::Deploy { path, host, deb } => deploy::run(&path, host.as_deref(), deb.as_deref()), Command::Login => auth::login(), Command::Logout => auth::logout(), + Command::Bump { deb } => bump::run(deb.as_deref()), Command::Publish { deb } => publish::run(deb.as_deref()), Command::Unpublish { package,