1#![deny(missing_docs)]
8
9#[allow(deprecated)]
10use std::env::home_dir;
11use anyhow::{anyhow, Context, Result};
14use command_ext::CommandExtCheck;
15use std::{path::PathBuf, process::Command};
16
17pub mod data;
18
19#[cfg(unix)]
20pub const ISPM_NAME: &str = "ispm";
22#[cfg(windows)]
23pub const ISPM_NAME: &str = "ispm.exe";
25pub const NON_INTERACTIVE_FLAG: &str = "--non-interactive";
27const ISPM_NOT_FOUND_ERROR: &str = "Failed to run ispm. Ensure ispm is installed and in PATH, or set SIMICS_BASE environment variable.";
29
30pub struct Internal;
32
33impl Internal {
34 const PRODUCT_NAME: &'static str = "Intel Simics Package Manager";
36
37 const CFG_FILENAME: &'static str = "simics-package-manager.cfg";
39
40 fn app_data_path() -> Result<PathBuf> {
44 #[allow(deprecated)]
45 let home_dir = home_dir().ok_or_else(|| anyhow!("No home directory found"))?;
48
49 #[cfg(unix)]
50 return Ok(home_dir.join(".config").join(Self::PRODUCT_NAME));
51
52 #[cfg(windows)]
53 return Ok(home_dir
55 .join("AppData")
56 .join("Local")
57 .join(Self::PRODUCT_NAME));
58 }
59
60 pub fn cfg_file_path() -> Result<PathBuf> {
63 Ok(Self::app_data_path()?.join(Self::CFG_FILENAME))
64 }
65
66 pub fn is_internal() -> Result<bool> {
68 const IS_INTERNAL_MSG: &str = "This is an Intel internal release";
69
70 Ok(String::from_utf8(
71 Command::new(ISPM_NAME)
72 .arg("help")
73 .check()
74 .context(ISPM_NOT_FOUND_ERROR)?
75 .stdout,
76 )?
77 .contains(IS_INTERNAL_MSG))
78 }
79}
80
81pub trait ToArgs {
83 fn to_args(&self) -> Vec<String>;
85}
86
87pub mod ispm {
89 use std::{iter::repeat, path::PathBuf};
90
91 use typed_builder::TypedBuilder;
92
93 use crate::{ToArgs, NON_INTERACTIVE_FLAG};
94
95 #[derive(TypedBuilder, Clone, Debug)]
96 pub struct GlobalOptions {
98 #[builder(default, setter(into))]
99 pub package_repo: Vec<String>,
101 #[builder(default, setter(into, strip_option))]
102 pub install_dir: Option<PathBuf>,
104 #[builder(default, setter(into, strip_option))]
105 pub https_proxy: Option<String>,
107 #[builder(default, setter(into, strip_option))]
108 pub no_proxy: Option<String>,
110 #[builder(default = true)]
111 pub non_interactive: bool,
113 #[builder(default = false)]
114 pub trust_insecure_packages: bool,
117 #[builder(default, setter(into, strip_option))]
118 pub config_file: Option<PathBuf>,
120 #[builder(default = false)]
121 pub no_config_file: bool,
123 #[builder(default, setter(into, strip_option))]
124 pub temp_dir: Option<PathBuf>,
126 #[builder(default, setter(into, strip_option))]
127 pub auth_file: Option<PathBuf>,
129 }
130
131 impl ToArgs for GlobalOptions {
132 fn to_args(&self) -> Vec<String> {
133 let mut args = Vec::new();
134
135 args.extend(
136 repeat("--package-repo".to_string())
137 .zip(self.package_repo.iter())
138 .flat_map(|(flag, arg)| [flag, arg.to_string()]),
139 );
140 args.extend(self.install_dir.as_ref().iter().flat_map(|id| {
141 [
142 "--install-dir".to_string(),
143 id.to_string_lossy().to_string(),
144 ]
145 }));
146 args.extend(
147 self.https_proxy
148 .as_ref()
149 .iter()
150 .flat_map(|p| ["--https-proxy".to_string(), p.to_string()]),
151 );
152 args.extend(
153 self.no_proxy
154 .as_ref()
155 .iter()
156 .flat_map(|p| ["--no-proxy".to_string(), p.to_string()]),
157 );
158 if self.non_interactive {
159 args.push(NON_INTERACTIVE_FLAG.to_string())
160 }
161 if self.trust_insecure_packages {
162 args.push("--trust-insecure-packages".to_string())
163 }
164 args.extend(self.config_file.as_ref().iter().flat_map(|cf| {
165 [
166 "--config-file".to_string(),
167 cf.to_string_lossy().to_string(),
168 ]
169 }));
170 if self.no_config_file {
171 args.push("--no-config-file".to_string());
172 }
173 args.extend(
174 self.temp_dir
175 .as_ref()
176 .iter()
177 .flat_map(|td| ["--temp-dir".to_string(), td.to_string_lossy().to_string()]),
178 );
179 args.extend(
180 self.auth_file
181 .as_ref()
182 .iter()
183 .flat_map(|af| ["--auth-file".to_string(), af.to_string_lossy().to_string()]),
184 );
185
186 args
187 }
188 }
189
190 impl Default for GlobalOptions {
191 fn default() -> Self {
192 Self::builder().build()
193 }
194 }
195
196 pub mod packages {
198 use crate::{
199 data::{Packages, ProjectPackage},
200 ToArgs, ISPM_NAME, ISPM_NOT_FOUND_ERROR, NON_INTERACTIVE_FLAG,
201 };
202 use anyhow::{Context, Result};
203 use command_ext::CommandExtCheck;
204 use serde_json::from_slice;
205 use std::{collections::HashSet, iter::repeat, path::PathBuf, process::Command};
206 use typed_builder::TypedBuilder;
207
208 use super::GlobalOptions;
209
210 const PACKAGES_SUBCOMMAND: &str = "packages";
211
212 pub fn list(options: &GlobalOptions) -> Result<Packages> {
214 let mut packages: Packages = from_slice(
215 &Command::new(ISPM_NAME)
216 .arg(PACKAGES_SUBCOMMAND)
217 .arg(NON_INTERACTIVE_FLAG)
218 .arg("--list-installed")
223 .arg("--json")
224 .args(options.to_args())
225 .check()
226 .context(ISPM_NOT_FOUND_ERROR)?
227 .stdout,
228 )?;
229
230 packages.sort();
231
232 Ok(packages)
233 }
234
235 #[derive(TypedBuilder, Clone, Debug)]
236 pub struct InstallOptions {
238 #[builder(default, setter(into))]
239 pub packages: HashSet<ProjectPackage>,
241 #[builder(default, setter(into))]
242 pub package_paths: Vec<PathBuf>,
244 #[builder(default)]
245 pub global: GlobalOptions,
247 #[builder(default = false)]
248 pub install_all: bool,
250 }
251
252 impl ToArgs for InstallOptions {
253 fn to_args(&self) -> Vec<String> {
254 repeat("-i".to_string())
255 .zip(
256 self.packages.iter().map(|p| p.to_string()).chain(
257 self.package_paths
258 .iter()
259 .map(|p| p.to_string_lossy().to_string()),
260 ),
261 )
262 .flat_map(|(flag, arg)| [flag, arg])
263 .chain(self.global.to_args().iter().cloned())
264 .chain(self.install_all.then_some("--install-all".to_string()))
265 .collect::<Vec<_>>()
266 }
267 }
268
269 pub fn install(install_options: &InstallOptions) -> Result<()> {
271 Command::new(ISPM_NAME)
272 .arg(PACKAGES_SUBCOMMAND)
273 .args(install_options.to_args())
274 .arg(NON_INTERACTIVE_FLAG)
275 .check()
276 .context(ISPM_NOT_FOUND_ERROR)?;
277 Ok(())
278 }
279
280 #[derive(TypedBuilder, Clone, Debug)]
281 pub struct UninstallOptions {
283 #[builder(default, setter(into))]
284 packages: Vec<ProjectPackage>,
286 #[builder(default)]
287 global: GlobalOptions,
288 }
289
290 impl ToArgs for UninstallOptions {
291 fn to_args(&self) -> Vec<String> {
292 repeat("-u".to_string())
293 .zip(self.packages.iter().map(|p| p.to_string()))
294 .flat_map(|(flag, arg)| [flag, arg])
295 .chain(self.global.to_args().iter().cloned())
296 .collect::<Vec<_>>()
297 }
298 }
299
300 pub fn uninstall(uninstall_options: &UninstallOptions) -> Result<()> {
302 Command::new(ISPM_NAME)
303 .arg(PACKAGES_SUBCOMMAND)
304 .args(uninstall_options.to_args())
305 .arg(NON_INTERACTIVE_FLAG)
306 .check()
307 .context(ISPM_NOT_FOUND_ERROR)?;
308 Ok(())
309 }
310 }
311
312 pub mod projects {
314 use crate::{
315 data::{ProjectPackage, Projects},
316 ToArgs, ISPM_NAME, ISPM_NOT_FOUND_ERROR, NON_INTERACTIVE_FLAG,
317 };
318 use anyhow::{anyhow, Context, Result};
319 use command_ext::CommandExtCheck;
320 use serde_json::from_slice;
321 use std::{collections::HashSet, iter::once, path::Path, process::Command};
322 use typed_builder::TypedBuilder;
323
324 use super::GlobalOptions;
325
326 const IGNORE_EXISTING_FILES_FLAG: &str = "--ignore-existing-files";
327 const CREATE_PROJECT_FLAG: &str = "--create";
328 const PROJECTS_SUBCOMMAND: &str = "projects";
329
330 #[derive(TypedBuilder, Clone, Debug)]
331 pub struct CreateOptions {
333 #[builder(default, setter(into))]
334 packages: HashSet<ProjectPackage>,
335 #[builder(default = false)]
336 ignore_existing_files: bool,
337 #[builder(default)]
338 global: GlobalOptions,
339 }
340
341 impl ToArgs for CreateOptions {
342 fn to_args(&self) -> Vec<String> {
343 self.packages
344 .iter()
345 .map(|p| Some(p.to_string()))
346 .chain(once(
347 self.ignore_existing_files
348 .then_some(IGNORE_EXISTING_FILES_FLAG.to_string()),
349 ))
350 .flatten()
351 .chain(self.global.to_args().iter().cloned())
352 .collect::<Vec<_>>()
353 }
354 }
355
356 pub fn create<P>(create_options: &CreateOptions, project_path: P) -> Result<()>
358 where
359 P: AsRef<Path>,
360 {
361 let mut args = vec![
362 PROJECTS_SUBCOMMAND.to_string(),
363 project_path
364 .as_ref()
365 .to_str()
366 .ok_or_else(|| anyhow!("Could not convert to string"))?
367 .to_string(),
368 CREATE_PROJECT_FLAG.to_string(),
369 ];
370 args.extend(create_options.to_args());
371 Command::new(ISPM_NAME)
372 .args(args)
373 .check()
374 .context(ISPM_NOT_FOUND_ERROR)?;
375
376 Ok(())
377 }
378
379 pub fn list(options: &GlobalOptions) -> Result<Projects> {
381 Ok(from_slice(
382 &Command::new(ISPM_NAME)
383 .arg(PROJECTS_SUBCOMMAND)
384 .arg(NON_INTERACTIVE_FLAG)
385 .arg("--list")
390 .arg("--json")
391 .args(options.to_args())
392 .check()
393 .context(ISPM_NOT_FOUND_ERROR)?
394 .stdout,
395 )?)
396 }
397 }
398
399 pub mod platforms {
401 use crate::{data::Platforms, ISPM_NAME, ISPM_NOT_FOUND_ERROR, NON_INTERACTIVE_FLAG};
402 use anyhow::{Context, Result};
403 use command_ext::CommandExtCheck;
404 use serde_json::from_slice;
405 use std::process::Command;
406
407 const PLATFORMS_SUBCOMMAND: &str = "platforms";
408
409 pub fn list() -> Result<Platforms> {
411 Ok(from_slice(
412 &Command::new(ISPM_NAME)
413 .arg(PLATFORMS_SUBCOMMAND)
414 .arg(NON_INTERACTIVE_FLAG)
415 .arg("--list")
420 .arg("--json")
421 .check()
422 .context(ISPM_NOT_FOUND_ERROR)?
423 .stdout,
424 )?)
425 }
426 }
427
428 pub mod settings {
430 use crate::{data::Settings, ISPM_NAME, ISPM_NOT_FOUND_ERROR, NON_INTERACTIVE_FLAG};
431 use anyhow::{Context, Result};
432 use command_ext::CommandExtCheck;
433 use serde_json::from_slice;
434 use std::process::Command;
435
436 const SETTINGS_SUBCOMMAND: &str = "settings";
437
438 pub fn list() -> Result<Settings> {
440 from_slice(
441 &Command::new(ISPM_NAME)
442 .arg(SETTINGS_SUBCOMMAND)
443 .arg(NON_INTERACTIVE_FLAG)
444 .arg("--json")
445 .check()
446 .context(ISPM_NOT_FOUND_ERROR)?
447 .stdout,
448 )
449 .or_else(|_| {
450 Settings::get()
452 })
453 }
454 }
455}
456
457#[cfg(test)]
458mod test {
459 use anyhow::Result;
460 use std::path::PathBuf;
461
462 use crate::{
463 data::{IPathObject, ProxySettingTypes, RepoPath, Settings},
464 ispm::{self, GlobalOptions},
465 };
466 use serde_json::from_str;
467
468 #[test]
469 fn test_simple_public() {
470 let expected = Settings::builder()
471 .archives([RepoPath::builder()
472 .value("https://artifactory.example.com/artifactory/repos/example/")
473 .enabled(true)
474 .priority(0)
475 .id(0)
476 .build()])
477 .install_path(
478 IPathObject::builder()
479 .id(1)
480 .priority(0)
481 .value("/home/user/simics")
482 .enabled(true)
483 .writable(true)
484 .build(),
485 )
486 .cfg_version(2)
487 .temp_directory(PathBuf::from("/home/user/tmp"))
488 .manifest_repos([
489 IPathObject::builder()
490 .id(0)
491 .priority(0)
492 .value("https://x.y.example.com")
493 .enabled(true)
494 .writable(false)
495 .build(),
496 IPathObject::builder()
497 .id(1)
498 .priority(1)
499 .value("https://artifactory.example.com/artifactory/repos/example/")
500 .enabled(true)
501 .build(),
502 ])
503 .projects([IPathObject::builder()
504 .id(0)
505 .priority(0)
506 .value("/home/user/simics-projects/qsp-x86-project")
507 .enabled(true)
508 .build()])
509 .key_store([IPathObject::builder()
510 .id(0)
511 .priority(0)
512 .value("/home/user/simics/keys")
513 .enabled(true)
514 .build()])
515 .proxy_settings_to_use(ProxySettingTypes::Env)
516 .build();
517 const SETTINGS_TEST_SIMPLE_PUBLIC: &str =
518 include_str!("../tests/config/simple-public/simics-package-manager.cfg");
519
520 let settings: Settings = from_str(SETTINGS_TEST_SIMPLE_PUBLIC)
521 .unwrap_or_else(|e| panic!("Error loading simple configuration: {e}"));
522
523 assert_eq!(settings, expected)
524 }
525
526 #[test]
527 fn test_current() -> Result<()> {
528 ispm::settings::list()?;
529 Ok(())
530 }
531
532 #[test]
533 fn test_packages() -> Result<()> {
534 ispm::packages::list(&GlobalOptions::default())?;
535 Ok(())
536 }
537}