#![deny(missing_docs)]
#[allow(deprecated)]
use std::env::home_dir;
use anyhow::{anyhow, Context, Result};
use command_ext::CommandExtCheck;
use std::{path::PathBuf, process::Command};
pub mod data;
#[cfg(unix)]
pub const ISPM_NAME: &str = "ispm";
#[cfg(windows)]
pub const ISPM_NAME: &str = "ispm.exe";
pub const NON_INTERACTIVE_FLAG: &str = "--non-interactive";
const ISPM_NOT_FOUND_ERROR: &str = "Failed to run ispm. Ensure ispm is installed and in PATH, or set SIMICS_BASE environment variable.";
pub struct Internal;
impl Internal {
const PRODUCT_NAME: &'static str = "Intel Simics Package Manager";
const CFG_FILENAME: &'static str = "simics-package-manager.cfg";
fn app_data_path() -> Result<PathBuf> {
#[allow(deprecated)]
let home_dir = home_dir().ok_or_else(|| anyhow!("No home directory found"))?;
#[cfg(unix)]
return Ok(home_dir.join(".config").join(Self::PRODUCT_NAME));
#[cfg(windows)]
return Ok(home_dir
.join("AppData")
.join("Local")
.join(Self::PRODUCT_NAME));
}
pub fn cfg_file_path() -> Result<PathBuf> {
Ok(Self::app_data_path()?.join(Self::CFG_FILENAME))
}
pub fn is_internal() -> Result<bool> {
const IS_INTERNAL_MSG: &str = "This is an Intel internal release";
Ok(String::from_utf8(
Command::new(ISPM_NAME)
.arg("help")
.check()
.context(ISPM_NOT_FOUND_ERROR)?
.stdout,
)?
.contains(IS_INTERNAL_MSG))
}
}
pub trait ToArgs {
fn to_args(&self) -> Vec<String>;
}
pub mod ispm {
use std::{iter::repeat, path::PathBuf};
use typed_builder::TypedBuilder;
use crate::{ToArgs, NON_INTERACTIVE_FLAG};
#[derive(TypedBuilder, Clone, Debug)]
pub struct GlobalOptions {
#[builder(default, setter(into))]
pub package_repo: Vec<String>,
#[builder(default, setter(into, strip_option))]
pub install_dir: Option<PathBuf>,
#[builder(default, setter(into, strip_option))]
pub https_proxy: Option<String>,
#[builder(default, setter(into, strip_option))]
pub no_proxy: Option<String>,
#[builder(default = true)]
pub non_interactive: bool,
#[builder(default = false)]
pub trust_insecure_packages: bool,
#[builder(default, setter(into, strip_option))]
pub config_file: Option<PathBuf>,
#[builder(default = false)]
pub no_config_file: bool,
#[builder(default, setter(into, strip_option))]
pub temp_dir: Option<PathBuf>,
#[builder(default, setter(into, strip_option))]
pub auth_file: Option<PathBuf>,
}
impl ToArgs for GlobalOptions {
fn to_args(&self) -> Vec<String> {
let mut args = Vec::new();
args.extend(
repeat("--package-repo".to_string())
.zip(self.package_repo.iter())
.flat_map(|(flag, arg)| [flag, arg.to_string()]),
);
args.extend(self.install_dir.as_ref().iter().flat_map(|id| {
[
"--install-dir".to_string(),
id.to_string_lossy().to_string(),
]
}));
args.extend(
self.https_proxy
.as_ref()
.iter()
.flat_map(|p| ["--https-proxy".to_string(), p.to_string()]),
);
args.extend(
self.no_proxy
.as_ref()
.iter()
.flat_map(|p| ["--no-proxy".to_string(), p.to_string()]),
);
if self.non_interactive {
args.push(NON_INTERACTIVE_FLAG.to_string())
}
if self.trust_insecure_packages {
args.push("--trust-insecure-packages".to_string())
}
args.extend(self.config_file.as_ref().iter().flat_map(|cf| {
[
"--config-file".to_string(),
cf.to_string_lossy().to_string(),
]
}));
if self.no_config_file {
args.push("--no-config-file".to_string());
}
args.extend(
self.temp_dir
.as_ref()
.iter()
.flat_map(|td| ["--temp-dir".to_string(), td.to_string_lossy().to_string()]),
);
args.extend(
self.auth_file
.as_ref()
.iter()
.flat_map(|af| ["--auth-file".to_string(), af.to_string_lossy().to_string()]),
);
args
}
}
impl Default for GlobalOptions {
fn default() -> Self {
Self::builder().build()
}
}
pub mod packages {
use crate::{
data::{Packages, ProjectPackage},
ToArgs, ISPM_NAME, ISPM_NOT_FOUND_ERROR, NON_INTERACTIVE_FLAG,
};
use anyhow::{Context, Result};
use command_ext::CommandExtCheck;
use serde_json::from_slice;
use std::{collections::HashSet, iter::repeat, path::PathBuf, process::Command};
use typed_builder::TypedBuilder;
use super::GlobalOptions;
const PACKAGES_SUBCOMMAND: &str = "packages";
pub fn list(options: &GlobalOptions) -> Result<Packages> {
let mut packages: Packages = from_slice(
&Command::new(ISPM_NAME)
.arg(PACKAGES_SUBCOMMAND)
.arg(NON_INTERACTIVE_FLAG)
.arg("--list-installed")
.arg("--json")
.args(options.to_args())
.check()
.context(ISPM_NOT_FOUND_ERROR)?
.stdout,
)?;
packages.sort();
Ok(packages)
}
#[derive(TypedBuilder, Clone, Debug)]
pub struct InstallOptions {
#[builder(default, setter(into))]
pub packages: HashSet<ProjectPackage>,
#[builder(default, setter(into))]
pub package_paths: Vec<PathBuf>,
#[builder(default)]
pub global: GlobalOptions,
#[builder(default = false)]
pub install_all: bool,
}
impl ToArgs for InstallOptions {
fn to_args(&self) -> Vec<String> {
repeat("-i".to_string())
.zip(
self.packages.iter().map(|p| p.to_string()).chain(
self.package_paths
.iter()
.map(|p| p.to_string_lossy().to_string()),
),
)
.flat_map(|(flag, arg)| [flag, arg])
.chain(self.global.to_args().iter().cloned())
.chain(self.install_all.then_some("--install-all".to_string()))
.collect::<Vec<_>>()
}
}
pub fn install(install_options: &InstallOptions) -> Result<()> {
Command::new(ISPM_NAME)
.arg(PACKAGES_SUBCOMMAND)
.args(install_options.to_args())
.arg(NON_INTERACTIVE_FLAG)
.check()
.context(ISPM_NOT_FOUND_ERROR)?;
Ok(())
}
#[derive(TypedBuilder, Clone, Debug)]
pub struct UninstallOptions {
#[builder(default, setter(into))]
packages: Vec<ProjectPackage>,
#[builder(default)]
global: GlobalOptions,
}
impl ToArgs for UninstallOptions {
fn to_args(&self) -> Vec<String> {
repeat("-u".to_string())
.zip(self.packages.iter().map(|p| p.to_string()))
.flat_map(|(flag, arg)| [flag, arg])
.chain(self.global.to_args().iter().cloned())
.collect::<Vec<_>>()
}
}
pub fn uninstall(uninstall_options: &UninstallOptions) -> Result<()> {
Command::new(ISPM_NAME)
.arg(PACKAGES_SUBCOMMAND)
.args(uninstall_options.to_args())
.arg(NON_INTERACTIVE_FLAG)
.check()
.context(ISPM_NOT_FOUND_ERROR)?;
Ok(())
}
}
pub mod projects {
use crate::{
data::{ProjectPackage, Projects},
ToArgs, ISPM_NAME, ISPM_NOT_FOUND_ERROR, NON_INTERACTIVE_FLAG,
};
use anyhow::{anyhow, Context, Result};
use command_ext::CommandExtCheck;
use serde_json::from_slice;
use std::{collections::HashSet, iter::once, path::Path, process::Command};
use typed_builder::TypedBuilder;
use super::GlobalOptions;
const IGNORE_EXISTING_FILES_FLAG: &str = "--ignore-existing-files";
const CREATE_PROJECT_FLAG: &str = "--create";
const PROJECTS_SUBCOMMAND: &str = "projects";
#[derive(TypedBuilder, Clone, Debug)]
pub struct CreateOptions {
#[builder(default, setter(into))]
packages: HashSet<ProjectPackage>,
#[builder(default = false)]
ignore_existing_files: bool,
#[builder(default)]
global: GlobalOptions,
}
impl ToArgs for CreateOptions {
fn to_args(&self) -> Vec<String> {
self.packages
.iter()
.map(|p| Some(p.to_string()))
.chain(once(
self.ignore_existing_files
.then_some(IGNORE_EXISTING_FILES_FLAG.to_string()),
))
.flatten()
.chain(self.global.to_args().iter().cloned())
.collect::<Vec<_>>()
}
}
pub fn create<P>(create_options: &CreateOptions, project_path: P) -> Result<()>
where
P: AsRef<Path>,
{
let mut args = vec![
PROJECTS_SUBCOMMAND.to_string(),
project_path
.as_ref()
.to_str()
.ok_or_else(|| anyhow!("Could not convert to string"))?
.to_string(),
CREATE_PROJECT_FLAG.to_string(),
];
args.extend(create_options.to_args());
Command::new(ISPM_NAME)
.args(args)
.check()
.context(ISPM_NOT_FOUND_ERROR)?;
Ok(())
}
pub fn list(options: &GlobalOptions) -> Result<Projects> {
Ok(from_slice(
&Command::new(ISPM_NAME)
.arg(PROJECTS_SUBCOMMAND)
.arg(NON_INTERACTIVE_FLAG)
.arg("--list")
.arg("--json")
.args(options.to_args())
.check()
.context(ISPM_NOT_FOUND_ERROR)?
.stdout,
)?)
}
}
pub mod platforms {
use crate::{data::Platforms, ISPM_NAME, ISPM_NOT_FOUND_ERROR, NON_INTERACTIVE_FLAG};
use anyhow::{Context, Result};
use command_ext::CommandExtCheck;
use serde_json::from_slice;
use std::process::Command;
const PLATFORMS_SUBCOMMAND: &str = "platforms";
pub fn list() -> Result<Platforms> {
Ok(from_slice(
&Command::new(ISPM_NAME)
.arg(PLATFORMS_SUBCOMMAND)
.arg(NON_INTERACTIVE_FLAG)
.arg("--list")
.arg("--json")
.check()
.context(ISPM_NOT_FOUND_ERROR)?
.stdout,
)?)
}
}
pub mod settings {
use crate::{data::Settings, ISPM_NAME, ISPM_NOT_FOUND_ERROR, NON_INTERACTIVE_FLAG};
use anyhow::{Context, Result};
use command_ext::CommandExtCheck;
use serde_json::from_slice;
use std::process::Command;
const SETTINGS_SUBCOMMAND: &str = "settings";
pub fn list() -> Result<Settings> {
from_slice(
&Command::new(ISPM_NAME)
.arg(SETTINGS_SUBCOMMAND)
.arg(NON_INTERACTIVE_FLAG)
.arg("--json")
.check()
.context(ISPM_NOT_FOUND_ERROR)?
.stdout,
)
.or_else(|_| {
Settings::get()
})
}
}
}
#[cfg(test)]
mod test {
use anyhow::Result;
use std::path::PathBuf;
use crate::{
data::{IPathObject, ProxySettingTypes, RepoPath, Settings},
ispm::{self, GlobalOptions},
};
use serde_json::from_str;
#[test]
fn test_simple_public() {
let expected = Settings::builder()
.archives([RepoPath::builder()
.value("https://artifactory.example.com/artifactory/repos/example/")
.enabled(true)
.priority(0)
.id(0)
.build()])
.install_path(
IPathObject::builder()
.id(1)
.priority(0)
.value("/home/user/simics")
.enabled(true)
.writable(true)
.build(),
)
.cfg_version(2)
.temp_directory(PathBuf::from("/home/user/tmp"))
.manifest_repos([
IPathObject::builder()
.id(0)
.priority(0)
.value("https://x.y.example.com")
.enabled(true)
.writable(false)
.build(),
IPathObject::builder()
.id(1)
.priority(1)
.value("https://artifactory.example.com/artifactory/repos/example/")
.enabled(true)
.build(),
])
.projects([IPathObject::builder()
.id(0)
.priority(0)
.value("/home/user/simics-projects/qsp-x86-project")
.enabled(true)
.build()])
.key_store([IPathObject::builder()
.id(0)
.priority(0)
.value("/home/user/simics/keys")
.enabled(true)
.build()])
.proxy_settings_to_use(ProxySettingTypes::Env)
.build();
const SETTINGS_TEST_SIMPLE_PUBLIC: &str =
include_str!("../tests/config/simple-public/simics-package-manager.cfg");
let settings: Settings = from_str(SETTINGS_TEST_SIMPLE_PUBLIC)
.unwrap_or_else(|e| panic!("Error loading simple configuration: {e}"));
assert_eq!(settings, expected)
}
#[test]
fn test_current() -> Result<()> {
ispm::settings::list()?;
Ok(())
}
#[test]
fn test_packages() -> Result<()> {
ispm::packages::list(&GlobalOptions::default())?;
Ok(())
}
}