use std::path::{Path, PathBuf};
use crate::provider_v3::plan::SoftwareArtifact;
use crate::provider_v3::{Error, Operation, Result, WireReason};
use crate::setup_core::platform_of_this_host;
use crate::setup_core::software::{self, Delivery, Software};
use crate::harness_runtime::facts::Harness;
fn declared(harness: &Harness) -> Result<Software> {
match harness.software {
Some(
found @ Software {
delivery: Delivery::Artifacts(_),
..
},
) => Ok(found),
Some(Software {
delivery: Delivery::Manager { tool, reason },
command,
..
}) => Err(Error::refuse(
WireReason::UnsupportedOperation,
format!(
"{command} is installed by {tool}, which resolves a dependency closure: {reason}"
),
)),
None => Err(Error::refuse(
WireReason::UnsupportedOperation,
format!(
"{} does not implement the software lifecycle",
harness.provider_id
),
)),
}
}
fn program_directory(prefix: Option<&Path>, operation: Operation) -> Result<PathBuf> {
prefix.map(Path::to_path_buf).ok_or_else(|| {
Error::refuse(
WireReason::ProviderUnavailable,
format!(
"{operation} installs a program, which lives under --prefix, not under --target; \
name an absolute --prefix"
),
)
})
}
fn version_for(declared: &Software, asked: Option<&str>) -> Result<Software> {
declared.at(asked).ok_or_else(|| {
let wanted = asked.unwrap_or_default();
Error::refuse(
WireReason::UnsupportedOperation,
format!(
"this build names {} {}; it cannot install {wanted}, and installing one it does \
name instead would be answering a question nobody asked",
declared.command,
declared.versions().join(" and "),
),
)
})
}
fn release_of(declared: &Software, path: &Path) -> Result<Software> {
let digest = crate::setup_core::digest::of_file(path)?;
let (os, arch) = platform_of_this_host();
declared.for_bytes(os, arch, &digest).ok_or_else(|| {
Error::refuse(
WireReason::DigestMismatch,
format!(
"the artifact given is not a {} release this build names: it hashes to {digest}, \
and {} publishes {} for {os}/{arch}",
declared.command,
declared.command,
declared.versions().join(" and "),
),
)
})
}
pub(crate) fn plan(
harness: &Harness,
prefix: Option<&Path>,
operation: Operation,
software_version: Option<&str>,
) -> Result<(Vec<SoftwareArtifact>, Vec<String>)> {
let declared = version_for(&declared(harness)?, software_version)?;
let root = program_directory(prefix, operation)?;
let (os, arch) = platform_of_this_host();
let member = declared.member_on(os, arch);
let entry_point = format!(
"bin/{}",
crate::setup_core::software::exposed_name(declared.command, member)
);
let exposed = root.join(&entry_point);
let present = software::Present::under_named(&root, declared.command, member);
if operation == Operation::SoftwareRemove {
let mut effects = vec![
format!(
"remove {}, the {} tree this provider installed",
root.join(declared.version).display(),
declared.version
),
format!("remove {}", exposed.display()),
];
let kept: Vec<&str> = present
.versions
.iter()
.map(String::as_str)
.filter(|version| *version != declared.version)
.collect();
if !kept.is_empty() {
effects.push(format!(
"leave {} in place: this build pins {} and does not decide about versions it \
does not pin",
kept.join(", "),
declared.version
));
}
return Ok((Vec::new(), effects));
}
if operation == Operation::SoftwareUpdate && present.versions.is_empty() {
return Err(Error::refuse(
WireReason::ProviderUnavailable,
format!(
"there is no {} under {} to update; software_install is the operation that \
puts one there",
declared.command,
root.display()
),
));
}
let artifact = declared.artifact_for(os, arch)?;
let mut effects = Vec::new();
if present.holds(declared.version) {
effects.push(format!(
"replace {}, which is already installed",
declared.version
));
} else if let Some(running) = present.exposed.as_deref() {
effects.push(format!(
"move {} from {running} to {}, keeping {running} where it is",
declared.command, declared.version
));
} else if !present.versions.is_empty() {
effects.push(format!(
"install {} beside {}, which no entry point names",
declared.version,
present.versions.join(", ")
));
}
effects.extend([
format!(
"download {} ({} bytes) in the operation's own download phase",
artifact.url, artifact.bytes
),
format!("check those bytes against {}", artifact.sha256),
match artifact.shape {
software::Shape::Raw => format!(
"place them as {}",
root.join(declared.version).join(declared.command).display()
),
software::Shape::GzipTar | software::Shape::Zip => format!(
"extract them into {}, whose {} is the program",
root.join(declared.version).display(),
artifact.member
),
},
format!("point {} at it", exposed.display()),
]);
Ok((
vec![SoftwareArtifact {
platform: artifact.platform.to_owned(),
url: artifact.url.to_owned(),
sha256: artifact.sha256.to_owned(),
byte_length: artifact.bytes,
entry_point,
}],
effects,
))
}
pub(crate) fn apply(
harness: &Harness,
prefix: Option<&Path>,
operation: Operation,
downloaded: &[PathBuf],
) -> Result<serde_json::Value> {
let declared = declared(harness)?;
let root = program_directory(prefix, operation)?;
let control = root.join(harness.control_directory);
std::fs::create_dir_all(&control).map_err(|error| {
Error::refuse(
WireReason::ProviderUnavailable,
format!("--prefix {} cannot be created: {error}", root.display()),
)
})?;
let mut guard = crate::setup_core::lock::TargetLock::acquire(&control)?;
guard.annotate(&format!("{} {operation}", harness.provider_id))?;
let recovered = crate::setup_core::software::recover(&root)?;
if operation == Operation::SoftwareRemove {
if !downloaded.is_empty() {
return Err(Error::refuse(
WireReason::UnsupportedOperation,
"software_remove downloads nothing, so it takes no --software-artifact",
));
}
let removed = software::remove(&declared, &root)?;
return Ok(serde_json::json!({
"state": "verified",
"recovered": recovered,
"operation": operation.as_str(),
"command": declared.command,
"version": declared.version,
"removed": removed,
}));
}
let [path] = downloaded else {
return Err(Error::refuse(
WireReason::ProviderUnavailable,
format!(
"{operation} installs the 1 artifact the plan named, and {} were given; \
pass one --software-artifact per entry, in the plan's order",
downloaded.len()
),
));
};
if operation == Operation::SoftwareUpdate
&& software::Present::under_named(&root, declared.command, declared.member_here())
.versions
.is_empty()
{
return Err(Error::refuse(
WireReason::ProviderUnavailable,
format!(
"there is no {} under {} to update any more; software_install is the operation \
that puts one there",
declared.command,
root.display()
),
));
}
let declared = release_of(&declared, path)?;
let (os, arch) = platform_of_this_host();
let artifact = declared.artifact_for(os, arch)?;
let installed = software::install(&declared, artifact, path, &root)?;
Ok(serde_json::json!({
"state": "verified",
"recovered": recovered,
"operation": operation.as_str(),
"command": declared.command,
"version": installed.version,
"entry_point": format!(
"bin/{}",
crate::setup_core::software::exposed_name(declared.command, artifact.member)
),
"executable": installed.executable.to_string_lossy(),
"files": installed.files,
}))
}
pub(crate) fn launch(
harness: &Harness,
target: &Path,
prefix: Option<&Path>,
arguments: &[String],
) -> Result<serde_json::Value> {
if !harness.can_launch() {
return Err(Error::refuse(
WireReason::UnsupportedOperation,
format!(
"{} does not declare launch: {}",
harness.provider_id,
harness.why_no_launch()
),
));
}
let declared = declared(harness)?;
let root = program_directory(prefix, Operation::Launch)?;
let executable = root.join("bin").join(crate::setup_core::software::exposed_name(
declared.command,
declared.member_here(),
));
let found = executable.metadata().map_err(|error| {
Error::refuse(
WireReason::ProviderUnavailable,
format!(
"{} is not installed under {}: {error}; run software_install first",
declared.command,
root.display()
),
)
})?;
if !found.is_file() {
return Err(Error::refuse(
WireReason::ProviderUnavailable,
format!("{} is not a regular file", executable.display()),
));
}
if !is_executable(&found) {
return Err(Error::refuse(
WireReason::ProviderUnavailable,
format!(
"{} exists but this host cannot execute it",
executable.display()
),
));
}
verify_installed(&root, declared.command)?;
#[allow(
clippy::disallowed_types,
reason = "launch starts the product, and is declared as doing so"
)]
let mut command = std::process::Command::new(&executable);
command.args(arguments);
for (name, value) in launch_environment(harness, target) {
command.env(name, value);
}
Err(replace_this_process(command, &executable))
}
fn verify_installed(root: &Path, command: &str) -> Result<()> {
let Some(manifest) = crate::setup_core::software::Manifest::read(root, command) else {
return Ok(());
};
let executable = root.join(&manifest.executable);
let found = crate::setup_core::digest::of_file(&executable).map_err(|error| {
Error::refuse(
WireReason::ProviderUnavailable,
format!(
"{} names {} and it could not be read: {error}",
crate::setup_core::software::Manifest::path(root, command).display(),
manifest.executable
),
)
})?;
if found != manifest.executable_sha256 {
return Err(Error::refuse(
WireReason::ProviderUnavailable,
format!(
"{} is not the {} this provider installed: recorded {}, found {}. \
Nothing has been started. Re-run software_install to put the pinned \
bytes back, or software_remove to take the prefix off.",
manifest.executable, manifest.version, manifest.executable_sha256, found
),
));
}
Ok(())
}
fn launch_environment(harness: &Harness, target: &Path) -> Vec<(&'static str, String)> {
let mut pairs = vec![(
harness.config_home_env,
target.to_string_lossy().into_owned(),
)];
if !harness.updates_off_env.is_empty() {
pairs.push((harness.updates_off_env, "1".to_owned()));
}
pairs
}
#[cfg(unix)]
fn is_executable(found: &std::fs::Metadata) -> bool {
use std::os::unix::fs::MetadataExt;
found.mode() & 0o111 != 0
}
#[cfg(not(unix))]
fn is_executable(_found: &std::fs::Metadata) -> bool {
true
}
#[cfg(unix)]
#[allow(
clippy::disallowed_types,
reason = "the command `launch` built, handed to exec"
)]
fn replace_this_process(mut command: std::process::Command, executable: &Path) -> Error {
use std::os::unix::process::CommandExt;
let failure = command.exec();
Error::refuse(
WireReason::ProviderUnavailable,
format!("{} could not be started: {failure}", executable.display()),
)
}
#[cfg(not(unix))]
fn replace_this_process(mut command: std::process::Command, executable: &Path) -> Error {
match command.status() {
Ok(status) => std::process::exit(status.code().unwrap_or(1)),
Err(failure) => Error::refuse(
WireReason::ProviderUnavailable,
format!("{} could not be started: {failure}", executable.display()),
),
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::panic)]
use std::fs;
use super::*;
#[test]
fn a_launch_refuses_an_executable_that_is_not_the_one_installed() {
let root = std::env::temp_dir().join(format!(
"harness-runtime-verify-{}-{}",
std::process::id(),
line!()
));
let _ = fs::remove_dir_all(&root);
let version = root.join("1.2.3");
fs::create_dir_all(&version).unwrap();
fs::create_dir_all(root.join("bin")).unwrap();
let executable = version.join("program");
fs::write(&executable, b"the installed bytes\n").unwrap();
assert!(
verify_installed(&root, "program").is_ok(),
"a prefix with no record was refused, which would condemn every \
installation made by an earlier release"
);
let manifest = crate::setup_core::software::Manifest {
schema_version: 1,
version: "1.2.3".to_owned(),
executable: "1.2.3/program".to_owned(),
executable_sha256: crate::setup_core::digest::of_file(&executable).unwrap(),
};
fs::write(
crate::setup_core::software::Manifest::path(&root, "program"),
serde_json::to_vec(&manifest).unwrap(),
)
.unwrap();
assert!(
verify_installed(&root, "program").is_ok(),
"the bytes named by the record were refused"
);
fs::write(&executable, b"the installed bytes?\n").unwrap();
let refused = verify_installed(&root, "program").unwrap_err();
assert_eq!(refused.reason(), Some(WireReason::ProviderUnavailable));
assert!(
refused.detail().contains("recorded") && refused.detail().contains("found"),
"the refusal does not say which digest was expected: {}",
refused.detail()
);
let _ = fs::remove_dir_all(&root);
}
#[test]
fn a_launch_sets_the_updater_switch_only_where_the_product_has_one() {
let target = Path::new("/tmp/nddev-launch-environment");
let with = Harness {
config_home_env: "PRODUCT_CONFIG_DIR",
updates_off_env: "DISABLE_UPDATES",
..crate::harness_runtime::wire::tests_support::TEST
};
assert_eq!(
launch_environment(&with, target),
vec![
("PRODUCT_CONFIG_DIR", target.display().to_string()),
("DISABLE_UPDATES", "1".to_owned()),
]
);
let without = Harness {
config_home_env: "PRODUCT_CONFIG_DIR",
updates_off_env: "",
..crate::harness_runtime::wire::tests_support::TEST
};
assert_eq!(
launch_environment(&without, target),
vec![("PRODUCT_CONFIG_DIR", target.display().to_string())],
"a product with no such variable had its environment written to anyway"
);
}
}