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