use std::{
borrow::Cow,
collections::{BTreeMap, HashMap},
path::PathBuf,
time::SystemTime,
};
use anyhow::Context;
use serde::Deserialize;
use crate::{
channel::Channel,
commands,
config::Config,
fault,
options::{InstallationOptions, IntentUpdate},
paths,
resolve::Intent,
state::{Installation, LocalState, PublicationId, PublicationRef},
utils,
version::{Authority, GitTarget},
};
pub fn install(
config: &Config,
channel: &Channel,
state: &mut LocalState,
options: &InstallationOptions,
) -> anyhow::Result<()> {
commands::setup_midenup(config)?;
let home = &config.midenup_home;
let publication_id = PublicationId::generate();
let publication = paths::publication_dir(home, &channel.name, &publication_id);
let intent = carry_migrated_intent(state, channel, effective_intent(state, channel, options));
let plan = crate::plan::build_plan(channel, &intent, config.target(), &publication)?;
let previous = previous_publication(config, state, &channel.name);
let stale = options.stale.clone();
let mut entry = crate::publish::JournalEntry::install(
channel.name.clone(),
previous_publication_id(state, &channel.name),
publication_id.clone(),
target_installation(config, channel, &intent, options, &publication_id, &plan)?,
);
crate::publish::journal::prepare(home, &entry)?;
fault::fail_at(fault::FaultPoint::PostPrepare)?;
crate::install::prepare(&publication)?;
if let Some((previous_dir, receipt)) = &previous {
crate::install::seed(
&plan,
&publication,
&crate::install::Seed {
publication: previous_dir,
receipt,
stale: &stale,
},
)?;
}
let realized = crate::install::execute(
&plan,
&publication,
crate::report::subprocess_output_visible(),
config.debug,
)?;
for step in &plan.steps {
if let Some(authority) = step.authority() {
crate::plan::recheck_path(authority)?;
}
}
fault::fail_at(fault::FaultPoint::PostStage)?;
crate::install::verify(&plan, &publication)?;
let receipt = crate::publish::receipt_for(
&plan,
&publication,
&publication_id,
&realized,
previous.as_ref().map(|(_, receipt)| receipt),
);
crate::publish::write_receipt(&publication, &receipt)?;
if let Some(installation) = entry.target_installation.as_mut() {
refresh_path_modification_times(config, &mut installation.components);
}
crate::publish::journal::prepare(home, &entry)?;
fault::fail_at(fault::FaultPoint::PostVerify)?;
crate::publish::journal::commit_symlink(home, &entry)?;
fault::fail_at(fault::FaultPoint::PostCommit)?;
crate::publish::journal::record(home, &entry, state)?;
fault::fail_at(fault::FaultPoint::PostRecord)?;
let relative_channel_target = PathBuf::from(format!("{}", channel.name));
for network in config.upstream_manifest()?.networks_for(&channel.name) {
if crate::plan::validate_artifact_id(network).is_err() {
continue;
}
let link = paths::network_link(home, network);
crate::trace!("linking {} -> {}", link.display(), relative_channel_target.display());
utils::fs::replace_symlink(&link, &relative_channel_target).with_context(|| {
format!("failed to point '{network}' at the newly installed channel")
})?;
}
fault::fail_at(fault::FaultPoint::PostDerive)?;
crate::publish::journal::clean(home, &entry)?;
crate::info!("installed channel '{}'", channel.name);
Ok(())
}
pub(crate) fn effective_intent(
state: &LocalState,
channel: &Channel,
options: &InstallationOptions,
) -> Intent {
let requested = Intent {
profiles: [options.profile].into_iter().collect(),
roots: options.components.iter().cloned().collect(),
};
let previous = state.get(&channel.name).map(|installation| installation.intent.clone());
match options.intent_update.clone() {
None => requested,
Some(IntentUpdate::Replace(intent)) => intent,
Some(IntentUpdate::Union(intent)) => {
let mut merged = previous.unwrap_or_default();
merged.union_with(&intent);
merged
},
Some(IntentUpdate::Preserve) => previous.unwrap_or(requested),
}
}
fn carry_migrated_intent(state: &LocalState, channel: &Channel, intent: Intent) -> Intent {
use colored::Colorize;
let Some(migrated) = state.get(&channel.name).filter(|installation| !installation.is_managed())
else {
return intent;
};
let mut intent = intent;
intent.union_with(&migrated.intent);
let (kept, dropped): (Vec<String>, Vec<String>) = intent
.roots
.iter()
.cloned()
.partition(|root| channel.get_component(root).is_some());
if dropped.is_empty() {
return intent;
}
crate::report::prepare_stderr_color();
let listed = dropped.iter().map(|root| format!("\n- {}", root.bold())).collect::<String>();
crate::warn!(
"these components are no longer part of channel {} and have been dropped from your \
selection:{listed}",
channel.name
);
Intent {
profiles: intent.profiles,
roots: kept.into_iter().collect(),
}
}
fn target_installation(
config: &Config,
channel: &Channel,
intent: &Intent,
options: &InstallationOptions,
publication_id: &PublicationId,
plan: &crate::plan::InstallationPlan,
) -> anyhow::Result<Installation> {
let installed_components = {
let mut installed_components: Vec<crate::manifest::Component> =
crate::resolve::resolve(channel, intent)?.into_iter().cloned().collect();
for component in installed_components.iter_mut() {
match &component.version {
Authority::Git {
repository_url,
subpath,
target: GitTarget::Branch { name, .. },
} => {
let revision_hash = utils::git::find_latest_hash(repository_url, name).ok();
component.version = Authority::Git {
repository_url: repository_url.clone(),
subpath: subpath.clone(),
target: GitTarget::Branch {
name: name.clone(),
latest_revision: revision_hash,
},
}
},
Authority::Git { .. } | Authority::Path { .. } | Authority::Registry { .. } => (),
}
}
refresh_path_modification_times(config, &mut installed_components);
for component in installed_components.iter_mut() {
if let Some(held) = options.held_back.iter().find(|held| held.name == component.name) {
*component = held.clone();
}
}
installed_components
};
Ok(Installation {
channel: channel.name.clone(),
intent: intent.clone(),
components: installed_components,
publication: PublicationRef::Managed {
id: publication_id.clone(),
plan_key: plan.key.clone(),
target: config.target().to_string(),
},
installed_at: chrono::Utc::now().timestamp(),
})
}
fn refresh_path_modification_times(config: &Config, components: &mut [crate::manifest::Component]) {
for component in components.iter_mut() {
let Authority::Path { path, .. } = &component.version else {
continue;
};
let path = if path.is_absolute() {
Cow::Borrowed(path.as_path())
} else {
Cow::Owned(config.working_directory.join(path.as_path()))
};
let latest_time = utils::fs::latest_modification(&path)
.ok()
.map(|(latest_modification, _)| latest_modification)
.unwrap_or_else(SystemTime::now);
component.version = Authority::Path {
path: path.to_path_buf(),
last_modification: Some(latest_time),
};
}
}
fn previous_publication_id(state: &LocalState, channel: &semver::Version) -> Option<PublicationId> {
match &state.get(channel)?.publication {
PublicationRef::Managed { id, .. } => Some(id.clone()),
PublicationRef::NeedsReinstall => None,
}
}
fn previous_publication(
config: &Config,
state: &LocalState,
channel: &semver::Version,
) -> Option<(PathBuf, crate::state::Receipt)> {
let installation = state.get(channel)?;
let PublicationRef::Managed { id, .. } = &installation.publication else {
return None;
};
let dir = paths::publication_dir(&config.midenup_home, channel, id);
let receipt = crate::publish::read_receipt(&dir).ok()?;
Some((dir, receipt))
}
#[allow(unused)]
pub struct InstalledBinary {
pub version: semver::Version,
pub location: Authority,
pub bins: Vec<String>,
pub features: Vec<String>,
}
#[derive(Deserialize)]
struct CargoInstalls {
#[serde(default)]
installs: BTreeMap<String, InstalledCrateInfo>,
}
#[derive(Deserialize)]
#[serde(untagged)]
enum InstalledCrateInfo {
Info {
#[serde(default)]
bins: Vec<String>,
#[serde(default)]
features: Vec<String>,
},
UnknownFormat,
}
#[allow(unused)]
pub fn get_installed_cargo_binaries(
root_dir: PathBuf,
) -> anyhow::Result<HashMap<String, InstalledBinary>> {
let crates2_json = root_dir.join(".crates2.json");
if !crates2_json.exists() {
return Ok(HashMap::new());
}
let crates2_json_file = std::fs::File::open(&crates2_json).with_context(|| {
format!(
"failed to obtain binaries installed via Cargo from '{}'",
crates2_json.display()
)
})?;
let installs =
serde_json::from_reader::<_, CargoInstalls>(crates2_json_file).with_context(|| {
format!("failed to deserialize Cargo's install manifest '{}'", crates2_json.display())
})?;
let mut installed = HashMap::new();
for (crate_id, info) in installs.installs {
let InstalledCrateInfo::Info { bins, features } = info else {
continue;
};
if bins.is_empty() {
continue;
}
let Some((crate_name, rest)) = crate_id.split_once(' ') else {
continue;
};
let Some((crate_version, source)) = rest.split_once(' ') else {
continue;
};
let crate_name = crate_name.trim();
let Ok(crate_version) = semver::Version::parse(crate_version.trim()) else {
continue;
};
let source = source.trim().trim_matches(['(', ')']);
if let Some(path) = source.strip_prefix("path+") {
installed.insert(
crate_name.to_string(),
InstalledBinary {
version: crate_version,
location: Authority::Path {
path: PathBuf::from(path.to_string()),
last_modification: None,
},
bins,
features,
},
);
} else if source.starts_with("registry+") {
let version = crate_version.clone();
installed.insert(
crate_name.to_string(),
InstalledBinary {
version: crate_version,
location: Authority::Registry { version },
bins,
features,
},
);
}
}
Ok(installed)
}