diff --git a/docs/bootstrap.md b/docs/bootstrap.md index 7620c7365f0..6b1468d9e42 100644 --- a/docs/bootstrap.md +++ b/docs/bootstrap.md @@ -179,7 +179,7 @@ only want to check one part without installing anything. | Config | Use for | | -------------------------------------------------------------- | ------------------------------------------------------------- | -| [`[bootstrap.packages]`](/bootstrap/packages/) | OS packages from apk, apt, dnf, pacman, brew, or mas | +| [`[bootstrap.packages]`](/bootstrap/packages/) | OS packages from apk, apt, dnf, pacman, brew, flatpak, or mas | | [`[bootstrap.repos]`](/bootstrap/repos.html) | Git repos cloned before dotfiles are applied | | [`[dotfiles]`](/dotfiles.html) | Whole-file dotfiles and small managed edits to existing files | | [`[bootstrap.mise_shell_activate]`](/bootstrap/shell.html) | mise activation snippets in shell startup files | diff --git a/docs/bootstrap/packages/flatpak.md b/docs/bootstrap/packages/flatpak.md new file mode 100644 index 00000000000..48969bfb76c --- /dev/null +++ b/docs/bootstrap/packages/flatpak.md @@ -0,0 +1,42 @@ +# Flatpak + +Flatpak applications and runtimes installed system-wide via the +[`flatpak`](https://docs.flatpak.org/en/latest/flatpak-command-reference.html) CLI. + +```toml +[bootstrap.packages] +"flatpak:org.mozilla.firefox" = "latest" +"flatpak:org.gnome.Builder" = "latest" +``` + +Flatpak packages are part of `[bootstrap.packages]`, just like apt packages, +Homebrew formulae, and Mac App Store apps. The package name is an application +or runtime ID accepted by `flatpak install` and `flatpak update`. + +mise does not install Flatpak or configure remotes implicitly. Install the +`flatpak` CLI and add the required remote (commonly Flathub) before applying +the config: + +```sh +flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo +mise bootstrap packages use flatpak:org.mozilla.firefox +``` + +## Commands + +```sh +mise bootstrap packages status --manager flatpak +mise bootstrap packages apply --manager flatpak +mise bootstrap packages upgrade --manager flatpak +``` + +mise manages the system-wide Flatpak installation. `apply` runs +`flatpak install --system --noninteractive ` for missing packages, while +`upgrade` runs `flatpak update --system --noninteractive ` for installed +packages. Flatpak resolves the configured ID from the system remotes. + +Flatpak does not expose installation of arbitrary historical versions through +these commands, so version pins are not supported. Use `"latest"` in config. + +The manager is Linux-only and requires `flatpak` on `PATH`. On other platforms, +or when the command is missing, shared configs list Flatpak entries as skipped. diff --git a/docs/bootstrap/packages/index.md b/docs/bootstrap/packages/index.md index 07eda87af65..cac9216801e 100644 --- a/docs/bootstrap/packages/index.md +++ b/docs/bootstrap/packages/index.md @@ -13,6 +13,7 @@ mise can ensure machine-global system packages are installed via the "brew:postgresql@17" = "latest" "brew:ffmpeg" = "latest" "brew-cask:firefox" = "latest" +"flatpak:org.mozilla.firefox" = "latest" "mas:497799835" = "latest" ``` @@ -42,15 +43,16 @@ declarative sections work the same way: ## Supported package managers -| Manager | Platform | Page | -| ----------- | -------------------------------------------------------------- | ----------------------------------------- | -| `apk` | Alpine Linux | [apk](/bootstrap/packages/apk.html) | -| `apt` | Debian, Ubuntu | [apt](/bootstrap/packages/apt.html) | -| `dnf` | Fedora, RHEL, CentOS, Rocky, Alma | [dnf](/bootstrap/packages/dnf.html) | -| `pacman` | Arch, Manjaro | [pacman](/bootstrap/packages/pacman.html) | -| `brew` | macOS (arm64), Linux (x86_64/arm64) — **no Homebrew required** | [brew](/bootstrap/packages/brew.html) | -| `brew-cask` | macOS — **no Homebrew required** | [brew](/bootstrap/packages/brew.html) | -| `mas` | macOS with the `mas` CLI on `PATH` | [mas](/bootstrap/packages/mas.html) | +| Manager | Platform | Page | +| ----------- | -------------------------------------------------------------- | ------------------------------------------- | +| `apk` | Alpine Linux | [apk](/bootstrap/packages/apk.html) | +| `apt` | Debian, Ubuntu | [apt](/bootstrap/packages/apt.html) | +| `dnf` | Fedora, RHEL, CentOS, Rocky, Alma | [dnf](/bootstrap/packages/dnf.html) | +| `pacman` | Arch, Manjaro | [pacman](/bootstrap/packages/pacman.html) | +| `brew` | macOS (arm64), Linux (x86_64/arm64) — **no Homebrew required** | [brew](/bootstrap/packages/brew.html) | +| `brew-cask` | macOS — **no Homebrew required** | [brew](/bootstrap/packages/brew.html) | +| `flatpak` | Linux with the `flatpak` CLI on `PATH` | [Flatpak](/bootstrap/packages/flatpak.html) | +| `mas` | macOS with the `mas` CLI on `PATH` | [mas](/bootstrap/packages/mas.html) | ## Semantics @@ -64,7 +66,8 @@ declarative sections work the same way: - **OS-filtered** — entries for a manager that isn't available on the current machine are not acted on, so the same config works across platforms: `apt` entries are ignored on macOS, `dnf` entries on Ubuntu, and so on. `brew` - works on both macOS and Linux; `brew-cask` works on macOS; `mas` works on + works on both macOS and Linux; `brew-cask` works on macOS; `flatpak` works + on Linux when the `flatpak` CLI is on `PATH`; `mas` works on macOS when the `mas` CLI is on `PATH`. Status commands still list unavailable managers so nothing is silently invisible. - **Manual installation only** — mise never installs system packages @@ -96,7 +99,7 @@ mise bootstrap packages apply --yes # skip the confirmation prompt mise bootstrap packages apply --manager apt mise bootstrap packages apply --update # refresh package manager metadata first -mise bootstrap packages use apk:zlib-dev apt:curl brew:jq brew-cask:firefox mas:497799835 +mise bootstrap packages use apk:zlib-dev apt:curl brew:jq brew-cask:firefox flatpak:org.mozilla.firefox mas:497799835 mise bootstrap packages use -g brew:ffmpeg # write globally mise bootstrap packages use apt:curl@8.5.0-2 # pin a version # (brew pins via the formula name instead: brew:postgresql@17) @@ -112,6 +115,7 @@ mise bootstrap packages prune --manager brew --yes mise bootstrap packages upgrade # upgrade installed packages to current versions mise bootstrap packages upgrade --manager brew mise bootstrap packages upgrade --manager brew-cask +mise bootstrap packages upgrade --manager flatpak mise bootstrap packages upgrade --manager mas ``` @@ -139,12 +143,12 @@ declarative cleanup command, similar in spirit to `mise bootstrap packages upgrade` refreshes package manager metadata and upgrades the configured packages that are already installed to the newest available version — apk, apt, and dnf also honor a version pinned in config (pacman, brew, -brew-cask, and mas [can't install pins](/bootstrap/packages/pacman.html), so +brew-cask, flatpak, and mas [can't install pins](/bootstrap/packages/pacman.html), so pinned entries are skipped with a warning). Packages that aren't installed yet are skipped — that's `mise bootstrap packages apply`'s job. For brew this pours the formula's current bottle and replaces the old keg; for -brew-cask this installs the current cask artifact; for mas this runs -`mas upgrade`. +brew-cask this installs the current cask artifact; for flatpak this updates the +configured applications and runtimes; for mas this runs `mas upgrade`. `mise doctor` also reports configured system packages and warns when any are missing. diff --git a/docs/cli/bootstrap/packages/apply.md b/docs/cli/bootstrap/packages/apply.md index 813a1986c04..c3167c15fe2 100644 --- a/docs/cli/bootstrap/packages/apply.md +++ b/docs/cli/bootstrap/packages/apply.md @@ -26,7 +26,7 @@ Packages in `manager:package` form; defaults to everything configured in [bootst ### `-m --manager ` -Only install packages for this manager, e.g. `apk`, `apt`, `brew`, `brew-cask`, or `mas` +Only install packages for this manager, e.g. `apk`, `apt`, `brew`, `brew-cask`, `flatpak`, or `mas` **Choices:** @@ -35,6 +35,7 @@ Only install packages for this manager, e.g. `apk`, `apt`, `brew`, `brew-cask`, - `brew` - `brew-cask` - `dnf` +- `flatpak` - `mas` - `pacman` @@ -54,7 +55,7 @@ Examples: ``` mise bootstrap packages apply -mise bootstrap packages apply apk:zlib-dev apt:curl brew:jq brew-cask:firefox mas:497799835 +mise bootstrap packages apply apk:zlib-dev apt:curl brew:jq brew-cask:firefox flatpak:org.mozilla.firefox mas:497799835 mise bootstrap packages apply --dry-run mise bootstrap packages apply --manager apt --yes ``` diff --git a/docs/cli/bootstrap/packages/upgrade.md b/docs/cli/bootstrap/packages/upgrade.md index 3bfc21e8a1b..814502f895a 100644 --- a/docs/cli/bootstrap/packages/upgrade.md +++ b/docs/cli/bootstrap/packages/upgrade.md @@ -11,7 +11,7 @@ Refreshes package manager metadata and upgrades the configured packages that are already installed: apk/apt/dnf/pacman upgrade to the newest available version (apk, apt, and dnf honor a version pinned in config), brew pours the formula's current bottle and replaces the old keg, brew-cask installs -the current cask artifact, and mas upgrades App Store apps. Packages that +the current cask artifact, flatpak updates applications and runtimes, and mas upgrades App Store apps. Packages that are not installed yet are skipped — use `mise bootstrap packages apply` for those. @@ -27,7 +27,7 @@ Packages in `manager:package` form; defaults to everything configured in [bootst ### `-m --manager ` -Only upgrade packages for this manager, e.g. `apk`, `apt`, `brew`, `brew-cask`, or `mas` +Only upgrade packages for this manager, e.g. `apk`, `apt`, `brew`, `brew-cask`, `flatpak`, or `mas` **Choices:** @@ -36,6 +36,7 @@ Only upgrade packages for this manager, e.g. `apk`, `apt`, `brew`, `brew-cask`, - `brew` - `brew-cask` - `dnf` +- `flatpak` - `mas` - `pacman` diff --git a/docs/cli/bootstrap/packages/use.md b/docs/cli/bootstrap/packages/use.md index 87726c31d14..9288bc88524 100644 --- a/docs/cli/bootstrap/packages/use.md +++ b/docs/cli/bootstrap/packages/use.md @@ -48,7 +48,7 @@ Skip the confirmation prompt Examples: ``` -mise bootstrap packages use apk:zlib-dev apt:curl brew:jq brew-cask:firefox mas:497799835 +mise bootstrap packages use apk:zlib-dev apt:curl brew:jq brew-cask:firefox flatpak:org.mozilla.firefox mas:497799835 mise bootstrap packages use -g brew:postgresql@17 mise bootstrap packages use apt:curl@8.5.0-2 ``` diff --git a/docs/tool-plugin-development.md b/docs/tool-plugin-development.md index b47eba534ba..339ec16cfbd 100644 --- a/docs/tool-plugin-development.md +++ b/docs/tool-plugin-development.md @@ -378,7 +378,7 @@ Optional fields: - **`version`** — a constraint (`>=3.0`, `>3`, `<=1.2`, `=3.0`, or a bare `3.0` meaning `>=3.0`) for `bin` and `pkgconfig`. mise runs ` --version` / `pkg-config --modversion` and compares. If a version can't be extracted, the dependency is treated as satisfied (presence is enough) rather than blocking the install. - **`optional`** — a short reason string. Missing optional dependencies never prompt or fail; they surface as a single informational line, letting users build without features they don't need (e.g. Erlang's `wxWidgets` GUI). -- **`packages`** — a map of package-manager name (`brew`, `brew-cask`, `apt`, `dnf`, `pacman`, `apk`, `mas`) to the package that provides the capability. +- **`packages`** — a map of package-manager name (`brew`, `brew-cask`, `apt`, `dnf`, `pacman`, `apk`, `flatpak`, `mas`) to the package that provides the capability. **Detection is the source of truth.** A check that passes is satisfied no matter how the capability was installed — Homebrew, apt, nix, MacPorts, or from source all pass without ceremony, and mise never asks _how_ it got there. The `packages` map is only consulted to _offer_ installing the missing subset; it is a remediation hint, not a declaration that the tool must come from that package manager. diff --git a/man/man1/mise.1 b/man/man1/mise.1 index 0aaa788f204..b28bb8f1e00 100644 --- a/man/man1/mise.1 +++ b/man/man1/mise.1 @@ -1056,7 +1056,7 @@ only. `install` is accepted as an alias for this command. .PP .TP \fB\-m, \-\-manager\fR \fI\fR -Only install packages for this manager, e.g. `apk`, `apt`, `brew`, `brew\-cask`, or `mas` +Only install packages for this manager, e.g. `apk`, `apt`, `brew`, `brew\-cask`, `flatpak`, or `mas` .TP \fB\-n, \-\-dry\-run\fR Print the commands that would run without running them @@ -1191,7 +1191,7 @@ Refreshes package manager metadata and upgrades the configured packages that are already installed: apk/apt/dnf/pacman upgrade to the newest available version (apk, apt, and dnf honor a version pinned in config), brew pours the formula's current bottle and replaces the old keg, brew\-cask installs -the current cask artifact, and mas upgrades App Store apps. Packages that +the current cask artifact, flatpak updates applications and runtimes, and mas upgrades App Store apps. Packages that are not installed yet are skipped — use `mise bootstrap packages apply` for those. @@ -1203,7 +1203,7 @@ Packages can also be given explicitly in `manager:package` form. .PP .TP \fB\-m, \-\-manager\fR \fI\fR -Only upgrade packages for this manager, e.g. `apk`, `apt`, `brew`, `brew\-cask`, or `mas` +Only upgrade packages for this manager, e.g. `apk`, `apt`, `brew`, `brew\-cask`, `flatpak`, or `mas` .TP \fB\-n, \-\-dry\-run\fR Print the commands that would run without running them diff --git a/mise.usage.kdl b/mise.usage.kdl index 88803de656c..3e33a767d12 100644 --- a/mise.usage.kdl +++ b/mise.usage.kdl @@ -493,14 +493,14 @@ only. `install` is accepted as an alias for this command. Examples: $ mise bootstrap packages apply - $ mise bootstrap packages apply apk:zlib-dev apt:curl brew:jq brew-cask:firefox mas:497799835 + $ mise bootstrap packages apply apk:zlib-dev apt:curl brew:jq brew-cask:firefox flatpak:org.mozilla.firefox mas:497799835 $ mise bootstrap packages apply --dry-run $ mise bootstrap packages apply --manager apt --yes """# - flag "-m --manager" help="Only install packages for this manager, e.g. `apk`, `apt`, `brew`, `brew-cask`, or `mas`" { + flag "-m --manager" help="Only install packages for this manager, e.g. `apk`, `apt`, `brew`, `brew-cask`, `flatpak`, or `mas`" { arg { - choices apk apt brew brew-cask dnf mas pacman + choices apk apt brew brew-cask dnf flatpak mas pacman } } flag "-n --dry-run" help="Print the commands that would run without running them" @@ -625,7 +625,7 @@ Refreshes package manager metadata and upgrades the configured packages that are already installed: apk/apt/dnf/pacman upgrade to the newest available version (apk, apt, and dnf honor a version pinned in config), brew pours the formula's current bottle and replaces the old keg, brew-cask installs -the current cask artifact, and mas upgrades App Store apps. Packages that +the current cask artifact, flatpak updates applications and runtimes, and mas upgrades App Store apps. Packages that are not installed yet are skipped — use `mise bootstrap packages apply` for those. @@ -642,9 +642,9 @@ Examples: $ mise bootstrap packages upgrade --dry-run """# - flag "-m --manager" help="Only upgrade packages for this manager, e.g. `apk`, `apt`, `brew`, `brew-cask`, or `mas`" { + flag "-m --manager" help="Only upgrade packages for this manager, e.g. `apk`, `apt`, `brew`, `brew-cask`, `flatpak`, or `mas`" { arg { - choices apk apt brew brew-cask dnf mas pacman + choices apk apt brew brew-cask dnf flatpak mas pacman } } flag "-n --dry-run" help="Print the commands that would run without running them" @@ -669,7 +669,7 @@ a mise version selector. mas uses numeric ADAM IDs and does not support pins. after_long_help #""" Examples: - $ mise bootstrap packages use apk:zlib-dev apt:curl brew:jq brew-cask:firefox mas:497799835 + $ mise bootstrap packages use apk:zlib-dev apt:curl brew:jq brew-cask:firefox flatpak:org.mozilla.firefox mas:497799835 $ mise bootstrap packages use -g brew:postgresql@17 $ mise bootstrap packages use apt:curl@8.5.0-2 diff --git a/src/cli/system/install.rs b/src/cli/system/install.rs index 2ade3ad4cdf..6c3fa035ad7 100644 --- a/src/cli/system/install.rs +++ b/src/cli/system/install.rs @@ -30,8 +30,8 @@ pub struct SystemInstall { #[clap(value_name = "PACKAGE")] packages: Vec, - /// Only install packages for this manager, e.g. `apk`, `apt`, `brew`, `brew-cask`, or `mas` - #[clap(long, short, value_parser = ["apk", "apt", "brew", "brew-cask", "dnf", "mas", "pacman"])] + /// Only install packages for this manager, e.g. `apk`, `apt`, `brew`, `brew-cask`, `flatpak`, or `mas` + #[clap(long, short, value_parser = ["apk", "apt", "brew", "brew-cask", "dnf", "flatpak", "mas", "pacman"])] manager: Option, /// Print the commands that would run without running them @@ -384,7 +384,7 @@ static AFTER_LONG_HELP: &str = color_print::cstr!( r#"Examples: $ mise bootstrap packages apply - $ mise bootstrap packages apply apk:zlib-dev apt:curl brew:jq brew-cask:firefox mas:497799835 + $ mise bootstrap packages apply apk:zlib-dev apt:curl brew:jq brew-cask:firefox flatpak:org.mozilla.firefox mas:497799835 $ mise bootstrap packages apply --dry-run $ mise bootstrap packages apply --manager apt --yes "# diff --git a/src/cli/system/upgrade.rs b/src/cli/system/upgrade.rs index 171bd09b0bc..0f7af56b572 100644 --- a/src/cli/system/upgrade.rs +++ b/src/cli/system/upgrade.rs @@ -10,7 +10,7 @@ use crate::system; /// that are already installed: apk/apt/dnf/pacman upgrade to the newest available /// version (apk, apt, and dnf honor a version pinned in config), brew pours the /// formula's current bottle and replaces the old keg, brew-cask installs -/// the current cask artifact, and mas upgrades App Store apps. Packages that +/// the current cask artifact, flatpak updates applications and runtimes, and mas upgrades App Store apps. Packages that /// are not installed yet are skipped — use `mise bootstrap packages apply` /// for those. /// @@ -23,8 +23,8 @@ pub struct SystemUpgrade { #[clap(value_name = "PACKAGE")] packages: Vec, - /// Only upgrade packages for this manager, e.g. `apk`, `apt`, `brew`, `brew-cask`, or `mas` - #[clap(long, short, value_parser = ["apk", "apt", "brew", "brew-cask", "dnf", "mas", "pacman"])] + /// Only upgrade packages for this manager, e.g. `apk`, `apt`, `brew`, `brew-cask`, `flatpak`, or `mas` + #[clap(long, short, value_parser = ["apk", "apt", "brew", "brew-cask", "dnf", "flatpak", "mas", "pacman"])] manager: Option, /// Print the commands that would run without running them diff --git a/src/cli/system/use.rs b/src/cli/system/use.rs index 9b37e6c549a..7fb9bd49a72 100644 --- a/src/cli/system/use.rs +++ b/src/cli/system/use.rs @@ -140,7 +140,7 @@ impl SystemUse { static AFTER_LONG_HELP: &str = color_print::cstr!( r#"Examples: - $ mise bootstrap packages use apk:zlib-dev apt:curl brew:jq brew-cask:firefox mas:497799835 + $ mise bootstrap packages use apk:zlib-dev apt:curl brew:jq brew-cask:firefox flatpak:org.mozilla.firefox mas:497799835 $ mise bootstrap packages use -g brew:postgresql@17 $ mise bootstrap packages use apt:curl@8.5.0-2 "# diff --git a/src/system/packages/flatpak.rs b/src/system/packages/flatpak.rs new file mode 100644 index 00000000000..41d9f291f1a --- /dev/null +++ b/src/system/packages/flatpak.rs @@ -0,0 +1,235 @@ +use std::collections::HashMap; +use std::process::Stdio; + +use async_trait::async_trait; +use eyre::bail; + +use super::{InstallOpts, PackageRequest, PackageState, PackageStatus, SystemPackageManager}; +use crate::result::Result; + +/// Flatpak applications and runtimes installed system-wide. +pub struct FlatpakManager {} + +impl FlatpakManager { + pub fn new() -> Self { + Self {} + } +} + +fn parse_flatpak_list(output: &str, requests: &[PackageRequest]) -> Vec { + let mut installed: HashMap<&str, Vec<&str>> = HashMap::new(); + for (application, version) in output.lines().filter_map(|line| line.split_once('\t')) { + installed.entry(application).or_default().push(version); + } + requests + .iter() + .map(|request| { + let state = match installed.get_mut(request.name.as_str()) { + Some(versions) => { + versions.sort_unstable(); + versions.dedup(); + let installed = versions + .iter() + .map(|version| { + if version.is_empty() { + "unknown" + } else { + version + } + }) + .collect::>() + .join(", "); + match &request.version { + Some(requested) if !versions.contains(&requested.as_str()) => { + PackageState::VersionMismatch { installed } + } + _ => PackageState::Installed { version: installed }, + } + } + None => PackageState::Missing, + }; + PackageStatus { + request: request.clone(), + state, + } + }) + .collect() +} + +async fn run_flatpak(args: &[String], action: &str) -> Result<()> { + debug!("$ flatpak {}", args.join(" ")); + let output = tokio::process::Command::new("flatpak") + .args(args) + .stdin(Stdio::null()) + .stdout(Stdio::inherit()) + .stderr(Stdio::piped()) + .output() + .await?; + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + bail!("flatpak {action} failed: {}", stderr.trim()); + } + Ok(()) +} + +#[async_trait(?Send)] +impl SystemPackageManager for FlatpakManager { + fn name(&self) -> &'static str { + "flatpak" + } + + fn is_available(&self) -> bool { + cfg!(target_os = "linux") && crate::file::which("flatpak").is_some() + } + + fn unavailable_reason(&self) -> String { + if cfg!(target_os = "linux") { + "flatpak not found".to_string() + } else { + "only available on linux".to_string() + } + } + + fn supports_version_pins(&self) -> bool { + false + } + + async fn installed(&self, pkgs: &[PackageRequest]) -> Result> { + if pkgs.is_empty() { + return Ok(vec![]); + } + let args = ["list", "--system", "--columns=application,version"]; + debug!("$ flatpak {}", args.join(" ")); + let output = tokio::process::Command::new("flatpak") + .args(args) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .await?; + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + bail!("flatpak list failed: {}", stderr.trim()); + } + Ok(parse_flatpak_list( + &String::from_utf8_lossy(&output.stdout), + pkgs, + )) + } + + async fn install(&self, pkgs: &[PackageRequest], opts: &InstallOpts) -> Result<()> { + if let Some(pkg) = pkgs.iter().find(|pkg| pkg.version.is_some()) { + bail!("flatpak cannot install a pinned version ('{pkg}')"); + } + let mut args = vec![ + "install".to_string(), + "--system".to_string(), + "--noninteractive".to_string(), + ]; + args.extend(pkgs.iter().map(|pkg| pkg.name.clone())); + if opts.dry_run { + miseprintln!("flatpak {}", args.join(" ")); + return Ok(()); + } + run_flatpak(&args, "install").await + } + + async fn upgrade(&self, pkgs: &[PackageRequest], opts: &InstallOpts) -> Result<()> { + if pkgs.is_empty() { + return Ok(()); + } + if let Some(pkg) = pkgs.iter().find(|pkg| pkg.version.is_some()) { + bail!("flatpak cannot upgrade a pinned version ('{pkg}')"); + } + let mut args = vec![ + "update".to_string(), + "--system".to_string(), + "--noninteractive".to_string(), + ]; + args.extend(pkgs.iter().map(|pkg| pkg.name.clone())); + if opts.dry_run { + miseprintln!("flatpak {}", args.join(" ")); + return Ok(()); + } + run_flatpak(&args, "update").await + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn req(name: &str, version: Option<&str>) -> PackageRequest { + PackageRequest { + name: name.to_string(), + version: version.map(str::to_string), + tap_url: None, + } + } + + #[test] + fn test_parse_flatpak_list() { + let statuses = parse_flatpak_list( + "org.mozilla.firefox\t128.0\norg.freedesktop.Platform\t24.08.1\n", + &[ + req("org.mozilla.firefox", None), + req("org.gnome.Builder", None), + ], + ); + assert_eq!( + statuses[0].state, + PackageState::Installed { + version: "128.0".to_string() + } + ); + assert_eq!(statuses[1].state, PackageState::Missing); + } + + #[test] + fn test_parse_flatpak_list_versions() { + let statuses = parse_flatpak_list( + "org.gnome.Sdk\t44.2\norg.gnome.Sdk\t43.1\norg.gnome.Sdk\t44.2\n", + &[ + req("org.gnome.Sdk", None), + req("org.gnome.Sdk", Some("43.1")), + req("org.gnome.Sdk", Some("45.0")), + ], + ); + assert_eq!( + statuses[0].state, + PackageState::Installed { + version: "43.1, 44.2".to_string() + } + ); + assert_eq!( + statuses[1].state, + PackageState::Installed { + version: "43.1, 44.2".to_string() + } + ); + assert_eq!( + statuses[2].state, + PackageState::VersionMismatch { + installed: "43.1, 44.2".to_string() + } + ); + } + + #[tokio::test] + async fn test_upgrade_rejects_empty_and_pinned_requests_before_running_flatpak() { + let manager = FlatpakManager::new(); + manager.upgrade(&[], &InstallOpts::default()).await.unwrap(); + + let err = manager + .upgrade( + &[req("org.mozilla.firefox", Some("128.0"))], + &InstallOpts::default(), + ) + .await + .unwrap_err(); + assert_eq!( + err.to_string(), + "flatpak cannot upgrade a pinned version ('org.mozilla.firefox@128.0')" + ); + } +} diff --git a/src/system/packages/mod.rs b/src/system/packages/mod.rs index 10c86d68dd1..289f40211ca 100644 --- a/src/system/packages/mod.rs +++ b/src/system/packages/mod.rs @@ -1,4 +1,4 @@ -//! System package managers (apk, apt, brew, brew-cask, mas) for the `[bootstrap.packages]` config section. +//! System package managers (apk, apt, brew, brew-cask, flatpak, mas) for the `[bootstrap.packages]` config section. //! //! These are machine-global, unversioned packages — deliberately separate from //! the `Backend` system, which manages per-project, version-pinned dev tools. @@ -14,6 +14,7 @@ pub mod apt; #[cfg(unix)] pub mod brew; pub mod dnf; +pub mod flatpak; pub mod mas; pub mod pacman; @@ -120,6 +121,7 @@ pub fn all_managers() -> Vec> { #[cfg(unix)] Arc::new(brew::BrewCaskManager::new()), Arc::new(dnf::DnfManager::new()), + Arc::new(flatpak::FlatpakManager::new()), Arc::new(mas::MasManager::new()), Arc::new(pacman::PacmanManager::new()), ]