mod install;
mod lock;
mod release;
mod source;
mod trust;
use std::path::{Path, PathBuf};
pub use lock::{DEFAULT_LOCK_FILE, LockEntry, LockFile, lock_key};
pub use release::IndexRow;
pub use source::{ARTIFACTS_URL_ENV, DEFAULT_ARTIFACTS_URL, DEFAULT_TAG, DOWNLOAD_TOKEN_ENV};
pub use trust::{
ALLOW_UNSIGNED_ENV, RELEASE_KEY_ID, RELEASE_PUBLIC_KEY, RELEASE_PUBLIC_KEY_HEX,
TRUSTED_KEYS_ENV, TrustPolicy, key_id, parse_key_hex, parse_signature_file, sha256_file,
sha256_hex, verify_signature,
};
use crate::error::{Error, Result};
pub(crate) use crate::library::minor_of;
use crate::registry::{Manifest, host_platform, install_dir_for, locate_in, search_path_for};
use release::{Release, Request};
use source::Source;
#[derive(Debug, Clone, Default)]
pub struct EnsureOptions {
pub dest: Option<PathBuf>,
pub platform: Option<String>,
pub url: Option<String>,
pub tag: Option<String>,
pub lock: Option<PathBuf>,
pub frozen: bool,
pub force: bool,
pub offline: bool,
pub trusted_keys: Option<Vec<String>>,
pub allow_unsigned: Option<bool>,
pub progress: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Action {
AlreadyInstalled,
Installed,
Replaced,
}
#[derive(Debug, Clone)]
pub struct Installed {
pub dir: PathBuf,
pub line: String,
pub version: String,
pub library: PathBuf,
pub library_sha256: String,
pub platform: String,
pub action: Action,
pub asset: Option<(String, String)>,
}
#[derive(Debug)]
pub struct Verification {
pub dir: PathBuf,
pub line: String,
pub version: String,
pub library: PathBuf,
pub expected: String,
pub result: Result<String>,
}
#[derive(Debug)]
pub struct ReleaseInfo {
pub origin: String,
pub signed_by: Option<String>,
pub license: String,
pub artifacts: Vec<IndexRow>,
}
pub fn ensure(line: &str, opts: &EnsureOptions) -> Result<Installed> {
let request = Request::parse(line)?;
let platform = platform_of(opts)?;
let dest = install_dir_for(&platform, opts.dest.as_deref());
let search = installed_search(opts, &platform, &dest);
let found = locate_in(&search, &request.minor).and_then(|dir| InstalledDir::read(&dir).ok());
if opts.offline && !opts.force {
if let Some(inst) = &found {
if request.accepts(&inst.manifest.clickhouse_version) {
let sha = inst.verify()?;
say(
opts,
&format!(
"already installed and verified: {}",
inst.library().display()
),
);
return Ok(inst.installed(&platform, sha, None));
}
}
}
let source = Source::resolve(opts.url.as_deref(), opts.tag.as_deref(), opts.offline)?;
let policy = policy_of(opts)?;
let release = Release::load(&source, &policy, opts.progress)?;
let row = release.select(&request, &platform)?;
let key = lock_key(&platform, &row.clickhouse_minor);
let mut lock = lock_of(opts)?;
if opts.frozen {
lock.as_ref()
.expect("frozen always has a lock")
.enforce(&key, row)?;
}
say(
opts,
&format!(
"ClickHouse {} -> line {} ({}), {platform}, from {}",
request.spelling, row.clickhouse_minor, row.clickhouse_version, release.origin
),
);
if !opts.force {
if let Some(inst) = &found {
if inst.manifest.clickhouse_version == row.clickhouse_version
&& inst.manifest.library == row.library
{
if let Ok(sha) = inst.verify() {
if sha == row.library_sha256 {
say(
opts,
&format!(
"already installed and verified: {}",
inst.library().display()
),
);
let installed = inst.installed(
&platform,
sha,
Some((row.file.clone(), row.sha256.clone())),
);
record(lock.as_mut(), opts, &key, row)?;
return Ok(installed);
}
}
}
}
}
let installed = install::fetch_and_install(&source, row, &dest, &platform, opts.progress)?;
record(lock.as_mut(), opts, &key, row)?;
Ok(installed)
}
pub fn ensure_all(opts: &EnsureOptions) -> Result<Vec<Installed>> {
let platform = platform_of(opts)?;
let dest = install_dir_for(&platform, opts.dest.as_deref());
let search = installed_search(opts, &platform, &dest);
let source = Source::resolve(opts.url.as_deref(), opts.tag.as_deref(), opts.offline)?;
let policy = policy_of(opts)?;
let release = Release::load(&source, &policy, opts.progress)?;
let rows = release.all(&platform);
if rows.is_empty() {
let mut platforms: Vec<String> = release.rows().iter().map(|r| r.platform()).collect();
platforms.sort();
platforms.dedup();
return Err(Error::ArtifactUnpublished {
requested: "every line".into(),
platform: platform.clone(),
origin: release.origin.clone(),
offered: if platforms.is_empty() {
"nothing".into()
} else {
format!("platforms {}", platforms.join(", "))
},
});
}
let mut lock = lock_of(opts)?;
let mut out = Vec::with_capacity(rows.len());
for row in rows {
row_complete(row)?;
release.cross_check(row)?;
let key = lock_key(&platform, &row.clickhouse_minor);
if opts.frozen {
lock.as_ref()
.expect("frozen always has a lock")
.enforce(&key, row)?;
}
let found = if opts.force {
None
} else {
locate_in(&search, &row.clickhouse_minor).and_then(|dir| InstalledDir::read(&dir).ok())
};
let confirmed = found.and_then(|inst| {
(inst.manifest.clickhouse_version == row.clickhouse_version
&& inst.manifest.library == row.library)
.then(|| {
inst.verify()
.ok()
.filter(|sha| *sha == row.library_sha256)
.map(|sha| (inst, sha))
})
.flatten()
});
let installed = match confirmed {
Some((inst, sha)) => {
say(
opts,
&format!(
"already installed and verified: {}",
inst.library().display()
),
);
inst.installed(&platform, sha, Some((row.file.clone(), row.sha256.clone())))
}
None => install::fetch_and_install(&source, row, &dest, &platform, opts.progress)?,
};
record(lock.as_mut(), opts, &key, row)?;
out.push(installed);
}
Ok(out)
}
pub fn verify_installed(dir: &Path) -> Vec<Verification> {
let mut out = Vec::new();
for (line, sub) in crate::registry::installed_lines(std::slice::from_ref(&dir.to_path_buf())) {
let inst = match InstalledDir::read(&sub) {
Ok(inst) => inst,
Err(e) => {
out.push(Verification {
dir: sub.clone(),
line,
version: String::new(),
library: sub.join("manifest.json"),
expected: String::new(),
result: Err(e),
});
continue;
}
};
out.push(Verification {
dir: sub.clone(),
line,
version: inst.manifest.clickhouse_version.clone(),
library: inst.library(),
expected: inst.manifest.library_sha256.clone(),
result: inst.verify(),
});
}
out
}
pub fn release_info(opts: &EnsureOptions) -> Result<ReleaseInfo> {
let source = Source::resolve(opts.url.as_deref(), opts.tag.as_deref(), opts.offline)?;
let policy = policy_of(opts)?;
let release = Release::load(&source, &policy, opts.progress)?;
Ok(ReleaseInfo {
origin: release.origin.clone(),
signed_by: release.signed_by.clone(),
license: release.license().0.to_string(),
artifacts: release.rows().to_vec(),
})
}
pub fn parse_line(line: &str) -> Result<String> {
Request::parse(line).map(|r| r.minor)
}
pub fn install_dir(opts: &EnsureOptions) -> Result<PathBuf> {
let platform = platform_of(opts)?;
Ok(install_dir_for(&platform, opts.dest.as_deref()))
}
pub fn search_path(opts: &EnsureOptions) -> Result<Vec<PathBuf>> {
let platform = platform_of(opts)?;
Ok(search_path_for(&platform, opts.dest.as_deref()))
}
fn installed_search(opts: &EnsureOptions, platform: &str, dest: &Path) -> Vec<PathBuf> {
match opts.dest {
Some(_) => vec![dest.to_path_buf()],
None => search_path_for(platform, None),
}
}
fn platform_of(opts: &EnsureOptions) -> Result<String> {
let platform = opts.platform.clone().unwrap_or_else(host_platform);
match platform.as_str() {
"linux-arm64" | "linux-amd64" | "darwin-arm64" | "darwin-amd64" => Ok(platform),
other => Err(Error::Fetch {
message: format!("not a known platform key: {other} ((linux|darwin)-(arm64|amd64))"),
}),
}
}
fn policy_of(opts: &EnsureOptions) -> Result<TrustPolicy> {
match (&opts.trusted_keys, opts.allow_unsigned) {
(None, None) => TrustPolicy::from_env(),
(keys, allow) => {
let env = TrustPolicy::from_env()?;
let keys_hex: Option<Vec<String>> = match keys {
Some(k) => Some(k.clone()),
None => std::env::var(TRUSTED_KEYS_ENV).ok().map(|list| {
list.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.map(String::from)
.collect()
}),
};
TrustPolicy::new(keys_hex.as_deref(), allow.unwrap_or(env.allow_unsigned()))
}
}
}
fn lock_of(opts: &EnsureOptions) -> Result<Option<LockFile>> {
match (&opts.lock, opts.frozen) {
(None, false) => Ok(None),
(Some(path), frozen) => LockFile::load(path, frozen).map(Some),
(None, true) => LockFile::load(Path::new(DEFAULT_LOCK_FILE), true).map(Some),
}
}
fn record(
lock: Option<&mut LockFile>,
opts: &EnsureOptions,
key: &str,
row: &IndexRow,
) -> Result<()> {
if let Some(lock) = lock {
if !opts.frozen && lock.record(key, row) {
lock.save()?;
say(opts, &format!("pinned {key} in {}", lock.path().display()));
}
}
Ok(())
}
fn row_complete(row: &IndexRow) -> Result<()> {
if row.file.is_empty()
|| row.sha256.is_empty()
|| row.library.is_empty()
|| row.library_sha256.is_empty()
{
return Err(Error::Fetch {
message: format!("index.json entry for {:?} is incomplete", row.file),
});
}
Ok(())
}
fn say(opts: &EnsureOptions, message: &str) {
if opts.progress {
eprintln!("chtypes: {message}");
}
}
struct InstalledDir {
dir: PathBuf,
manifest: Manifest,
}
impl InstalledDir {
fn read(dir: &Path) -> Result<InstalledDir> {
let path = dir.join("manifest.json");
let text = std::fs::read_to_string(&path).map_err(|e| Error::Fetch {
message: format!("{}: {e}", path.display()),
})?;
let manifest: Manifest = serde_json::from_str(&text).map_err(|e| Error::Fetch {
message: format!("{}: {e}", path.display()),
})?;
if manifest.library.is_empty() {
return Err(Error::Fetch {
message: format!("{} names no library", path.display()),
});
}
Ok(InstalledDir {
dir: dir.to_path_buf(),
manifest,
})
}
fn library(&self) -> PathBuf {
self.dir.join(&self.manifest.library)
}
fn verify(&self) -> Result<String> {
let library = self.library();
if self.manifest.library_sha256.is_empty() {
return Err(Error::Fetch {
message: format!(
"{} records no library_sha256",
self.dir.join("manifest.json").display()
),
});
}
let actual = sha256_file(&library).map_err(|e| Error::Fetch {
message: format!("{}: {e}", library.display()),
})?;
if actual != self.manifest.library_sha256 {
return Err(Error::ArtifactCorrupt {
subject: library.display().to_string(),
expected: self.manifest.library_sha256.clone(),
actual,
});
}
Ok(actual)
}
fn installed(&self, platform: &str, sha: String, asset: Option<(String, String)>) -> Installed {
let line = if self.manifest.clickhouse_minor.is_empty() {
minor_of(&self.manifest.clickhouse_version)
} else {
self.manifest.clickhouse_minor.clone()
};
Installed {
dir: self.dir.clone(),
line,
version: self.manifest.clickhouse_version.clone(),
library: self.library(),
library_sha256: sha,
platform: platform.to_string(),
action: Action::AlreadyInstalled,
asset,
}
}
}