Implement per-distro installation flow
This commit is contained in:
parent
caf870d05e
commit
b9b60e9b6c
21 changed files with 1109 additions and 33 deletions
|
|
@ -14,6 +14,7 @@ path = "src/main.rs"
|
|||
[dependencies]
|
||||
clap.workspace = true
|
||||
dialoguer.workspace = true
|
||||
libc.workspace = true
|
||||
aim-core = { path = "../aim-core" }
|
||||
|
||||
[dev-dependencies]
|
||||
|
|
|
|||
|
|
@ -2,15 +2,16 @@ pub mod cli;
|
|||
pub mod ui;
|
||||
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use aim_core::app::add::{AddPlan, build_add_plan, materialize_app_record};
|
||||
use aim_core::app::add::{
|
||||
AddPlan, InstalledApp, build_add_plan, install_app, resolve_requested_scope,
|
||||
};
|
||||
use aim_core::app::list::{ListRow, build_list_rows};
|
||||
use aim_core::app::remove::remove_registered_app;
|
||||
use aim_core::app::update::build_update_plan;
|
||||
use aim_core::domain::app::AppRecord;
|
||||
use aim_core::domain::source::SourceRef;
|
||||
use aim_core::domain::update::{ArtifactCandidate, UpdatePlan};
|
||||
use aim_core::domain::update::UpdatePlan;
|
||||
use aim_core::registry::model::Registry;
|
||||
use aim_core::registry::store::RegistryStore;
|
||||
|
||||
|
|
@ -22,6 +23,7 @@ pub fn parse() -> Cli {
|
|||
|
||||
pub fn dispatch(cli: Cli) -> Result<DispatchResult, DispatchError> {
|
||||
let registry_path = registry_path();
|
||||
let install_home = install_home(®istry_path);
|
||||
let store = RegistryStore::new(registry_path);
|
||||
let registry = store.load()?;
|
||||
let apps = registry.apps.clone();
|
||||
|
|
@ -46,29 +48,26 @@ pub fn dispatch(cli: Cli) -> Result<DispatchResult, DispatchError> {
|
|||
}
|
||||
|
||||
if let Some(query) = cli.query {
|
||||
let requested_scope = resolve_requested_scope(cli.system, cli.user, is_effective_root());
|
||||
let mut plan = build_add_plan(&query)?;
|
||||
if !plan.interactions.is_empty() {
|
||||
match ui::prompt::resolve_add_plan_interactions(plan.clone())? {
|
||||
Some(resolved) => {
|
||||
plan = resolved;
|
||||
}
|
||||
None => return Ok(DispatchResult::PendingAdd(plan)),
|
||||
None => return Ok(DispatchResult::PendingAdd(Box::new(plan))),
|
||||
}
|
||||
}
|
||||
|
||||
let record = materialize_app_record(&query, &plan)?;
|
||||
let installed = install_app(&query, &plan, &install_home, requested_scope)?;
|
||||
let mut updated_apps = registry.apps.clone();
|
||||
upsert_app_record(&mut updated_apps, record.clone());
|
||||
upsert_app_record(&mut updated_apps, installed.record.clone());
|
||||
store.save(&Registry {
|
||||
version: registry.version,
|
||||
apps: updated_apps,
|
||||
})?;
|
||||
|
||||
return Ok(DispatchResult::Added(AddedApp {
|
||||
record,
|
||||
selected_artifact: plan.selected_artifact,
|
||||
source: plan.resolution.source,
|
||||
}));
|
||||
return Ok(DispatchResult::Added(Box::new(installed)));
|
||||
}
|
||||
|
||||
Ok(DispatchResult::Noop)
|
||||
|
|
@ -89,25 +88,18 @@ fn registry_path() -> PathBuf {
|
|||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum DispatchResult {
|
||||
Added(AddedApp),
|
||||
Added(Box<InstalledApp>),
|
||||
List(Vec<ListRow>),
|
||||
PendingAdd(AddPlan),
|
||||
PendingAdd(Box<AddPlan>),
|
||||
Removed(String),
|
||||
UpdatePlan(UpdatePlan),
|
||||
Noop,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub struct AddedApp {
|
||||
pub record: AppRecord,
|
||||
pub selected_artifact: ArtifactCandidate,
|
||||
pub source: SourceRef,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DispatchError {
|
||||
AddPlan(aim_core::app::add::BuildAddPlanError),
|
||||
AddRecord(aim_core::app::add::MaterializeAddRecordError),
|
||||
AddInstall(aim_core::app::add::InstallAppError),
|
||||
Prompt(ui::prompt::PromptError),
|
||||
RemovePlan(aim_core::app::remove::ResolveRegisteredAppError),
|
||||
Registry(aim_core::registry::store::RegistryStoreError),
|
||||
|
|
@ -120,9 +112,9 @@ impl From<aim_core::app::add::BuildAddPlanError> for DispatchError {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<aim_core::app::add::MaterializeAddRecordError> for DispatchError {
|
||||
fn from(value: aim_core::app::add::MaterializeAddRecordError) -> Self {
|
||||
Self::AddRecord(value)
|
||||
impl From<aim_core::app::add::InstallAppError> for DispatchError {
|
||||
fn from(value: aim_core::app::add::InstallAppError) -> Self {
|
||||
Self::AddInstall(value)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -161,3 +153,32 @@ fn upsert_app_record(apps: &mut Vec<AppRecord>, record: AppRecord) {
|
|||
|
||||
apps.push(record);
|
||||
}
|
||||
|
||||
fn install_home(registry_path: &Path) -> PathBuf {
|
||||
if env::var_os("AIM_REGISTRY_PATH").is_some() {
|
||||
return registry_path
|
||||
.parent()
|
||||
.unwrap_or_else(|| Path::new("."))
|
||||
.join("install-home");
|
||||
}
|
||||
|
||||
let home = env::var_os("HOME").unwrap_or_else(|| ".".into());
|
||||
PathBuf::from(home)
|
||||
}
|
||||
|
||||
fn is_effective_root() -> bool {
|
||||
if let Some(value) = env::var_os("AIM_EFFECTIVE_ROOT") {
|
||||
let value = value.to_string_lossy();
|
||||
return value == "1" || value.eq_ignore_ascii_case("true");
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
unsafe {
|
||||
libc::geteuid() == 0
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,16 +19,35 @@ pub fn render_dispatch_result(result: &DispatchResult) -> String {
|
|||
}
|
||||
}
|
||||
|
||||
fn render_added_app(added: &crate::AddedApp) -> String {
|
||||
format!(
|
||||
"tracked app: {} ({})\nsource: {} {}\nselected artifact: {} [{}]",
|
||||
fn render_added_app(added: &aim_core::app::add::InstalledApp) -> String {
|
||||
let scope = match added.install_scope {
|
||||
aim_core::domain::app::InstallScope::User => "user",
|
||||
aim_core::domain::app::InstallScope::System => "system",
|
||||
};
|
||||
|
||||
let warning_lines = added
|
||||
.warnings
|
||||
.iter()
|
||||
.chain(added.install_outcome.warnings.iter())
|
||||
.map(|warning| format!("warning: {warning}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
||||
let summary = format!(
|
||||
"installing as {scope}\ninstalled app: {} ({})\nsource: {} {}\nselected artifact: {} [{}]",
|
||||
added.record.display_name,
|
||||
added.record.stable_id,
|
||||
added.source.kind.as_str(),
|
||||
added.source.locator,
|
||||
added.selected_artifact.url,
|
||||
added.selected_artifact.selection_reason,
|
||||
)
|
||||
);
|
||||
|
||||
if warning_lines.is_empty() {
|
||||
summary
|
||||
} else {
|
||||
format!("{summary}\n{warning_lines}")
|
||||
}
|
||||
}
|
||||
|
||||
fn render_pending_add(plan: &AddPlan) -> String {
|
||||
|
|
|
|||
|
|
@ -66,7 +66,8 @@ fn query_command_registers_unambiguous_app_in_registry_file() {
|
|||
.env(FIXTURE_MODE_ENV, "1")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("tracked app: bat (sharkdp-bat)"));
|
||||
.stdout(contains("installing as user"))
|
||||
.stdout(contains("installed app: bat (sharkdp-bat)"));
|
||||
|
||||
let contents = std::fs::read_to_string(®istry_path).unwrap();
|
||||
assert!(contents.contains("stable_id = \"sharkdp-bat\""));
|
||||
|
|
@ -103,9 +104,44 @@ fn old_release_query_can_track_latest_and_register_app() {
|
|||
.env("AIM_TRACKING_PREFERENCE", "latest")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("tracked app: t3code (pingdotgg-t3code)"));
|
||||
.stdout(contains("installing as user"))
|
||||
.stdout(contains("installed app: t3code (pingdotgg-t3code)"));
|
||||
|
||||
let contents = std::fs::read_to_string(®istry_path).unwrap();
|
||||
assert!(contents.contains("stable_id = \"pingdotgg-t3code\""));
|
||||
assert!(contents.contains("locator = \"pingdotgg/t3code\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_add_installs_and_renders_resolved_mode() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
let mut cmd = Command::cargo_bin("aim").unwrap();
|
||||
|
||||
cmd.arg("sharkdp/bat")
|
||||
.env("AIM_REGISTRY_PATH", ®istry_path)
|
||||
.env(FIXTURE_MODE_ENV, "1")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("installing as user"))
|
||||
.stdout(contains("installed app:"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn system_request_on_immutable_host_falls_back_to_user_install() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
let os_release_path = dir.path().join("os-release");
|
||||
std::fs::write(&os_release_path, "ID=fedora\nVARIANT_ID=silverblue\n").unwrap();
|
||||
|
||||
let mut cmd = Command::cargo_bin("aim").unwrap();
|
||||
|
||||
cmd.args(["--system", "sharkdp/bat"])
|
||||
.env("AIM_REGISTRY_PATH", ®istry_path)
|
||||
.env("AIM_OS_RELEASE_PATH", &os_release_path)
|
||||
.env(FIXTURE_MODE_ENV, "1")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("installing as user"))
|
||||
.stdout(contains("downgraded to user scope"));
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue