Skip to main content

ispm_wrapper/
lib.rs

1// Copyright (C) 2024 Intel Corporation
2// SPDX-License-Identifier: Apache-2.0
3
4//! Wrappers for the small subset of ISPM commands the fuzzer and its build processes need to
5//! function
6
7#![deny(missing_docs)]
8
9#[allow(deprecated)]
10use std::env::home_dir;
11// NOTE: Use of deprecated home_dir is ok because the "incorrect" windows behavior is actually
12// correct for SIMICS' use case.
13use anyhow::{anyhow, Context, Result};
14use command_ext::CommandExtCheck;
15use std::{path::PathBuf, process::Command};
16
17pub mod data;
18
19#[cfg(unix)]
20/// The name of the ispm executable
21pub const ISPM_NAME: &str = "ispm";
22#[cfg(windows)]
23/// The name of the ispm executable
24pub const ISPM_NAME: &str = "ispm.exe";
25/// The flag to use to run ISPM in non-interactive mode
26pub const NON_INTERACTIVE_FLAG: &str = "--non-interactive";
27/// Error message when ispm command fails to run
28const ISPM_NOT_FOUND_ERROR: &str = "Failed to run ispm. Ensure ispm is installed and in PATH, or set SIMICS_BASE environment variable.";
29
30/// Minimal implementation of internal ISPM functionality to use it externally
31pub struct Internal;
32
33impl Internal {
34    // NOTE: Can be found in package.json in extracted ispm application
35    const PRODUCT_NAME: &'static str = "Intel Simics Package Manager";
36
37    // NOTE: Can be found in `AppInfo` class in extracted ispm application
38    const CFG_FILENAME: &'static str = "simics-package-manager.cfg";
39
40    // NOTE: Can be found in `constructAppDataPath` in extracted ispm application
41    /// Retrieve the path to the directory containing ISPM's application data, in particular the
42    /// configuration file.
43    fn app_data_path() -> Result<PathBuf> {
44        #[allow(deprecated)]
45        // NOTE: Use of deprecated home_dir is ok because the "incorrect" windows behavior is actually
46        // correct for SIMICS' use case.
47        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        // This comes from the ispm source, it's hardcoded there and we hardcode it here
54        return Ok(home_dir
55            .join("AppData")
56            .join("Local")
57            .join(Self::PRODUCT_NAME));
58    }
59
60    // NOTE: Can be found in `getCfgFileName` in extracted ispm application
61    /// Retrieve the path to the ISPM configuration file
62    pub fn cfg_file_path() -> Result<PathBuf> {
63        Ok(Self::app_data_path()?.join(Self::CFG_FILENAME))
64    }
65
66    /// Returns whether this is an internal release of ISPM
67    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
81/// An implementor can convert itself into a list of command-line arguments
82pub trait ToArgs {
83    /// Convert this implementor into a list of command-line arguments
84    fn to_args(&self) -> Vec<String>;
85}
86
87/// Wrappers for ISPM commands
88pub 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    /// Global ISPM options
97    pub struct GlobalOptions {
98        #[builder(default, setter(into))]
99        /// A package repo to use when installing packages
100        pub package_repo: Vec<String>,
101        #[builder(default, setter(into, strip_option))]
102        /// A directory to install packages into, overriding global configurations
103        pub install_dir: Option<PathBuf>,
104        #[builder(default, setter(into, strip_option))]
105        /// An HTTPS proxy URL to use
106        pub https_proxy: Option<String>,
107        #[builder(default, setter(into, strip_option))]
108        /// A no-proxy string of addresses not to use the proxy for, e.g. "*.intel.com,127.0.0.1"
109        pub no_proxy: Option<String>,
110        #[builder(default = true)]
111        /// Whether this command should be run in non-interactive mode.
112        pub non_interactive: bool,
113        #[builder(default = false)]
114        /// Whether insecure packages should be trusted. This should be set to true when
115        /// installing an un-signed local package
116        pub trust_insecure_packages: bool,
117        #[builder(default, setter(into, strip_option))]
118        /// A path to an override configuration file
119        pub config_file: Option<PathBuf>,
120        #[builder(default = false)]
121        /// Whether the configuration file should not be used for this command
122        pub no_config_file: bool,
123        #[builder(default, setter(into, strip_option))]
124        /// A different temporary directory to use
125        pub temp_dir: Option<PathBuf>,
126        #[builder(default, setter(into, strip_option))]
127        /// An authentication file to use for this command
128        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    /// ISPM commands for package management
197    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        /// Get the currently installed and available packages
213        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                    // NOTE: There is a bug happening when running e.g.:
219                    // `ispm packages --list --json | cat > test.txt; stat -c '%s' test.txt`
220                    // where the output to the pipe from ISPM stops after the size of the
221                    // PIPE_BUF. For now, we mitigate this by passing `--list-installed` only.
222                    .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        /// Options that can be set when installing one or more packages
237        pub struct InstallOptions {
238            #[builder(default, setter(into))]
239            /// Packages to install by number/version
240            pub packages: HashSet<ProjectPackage>,
241            #[builder(default, setter(into))]
242            /// Packages to install by local path
243            pub package_paths: Vec<PathBuf>,
244            #[builder(default)]
245            /// Global ispm options
246            pub global: GlobalOptions,
247            #[builder(default = false)]
248            /// Whether to install all packages
249            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        /// Install a package or set of packages, executing the ispm command
270        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        /// Options that can be set when uninstalling one or more packages
282        pub struct UninstallOptions {
283            #[builder(default, setter(into))]
284            /// Packages to install by number/version
285            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        /// Uninstall a package or set of packages, executing the ispm command
301        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    /// ISPM commands for project management
313    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        /// Options that can be set when creating a project
332        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        /// Create a project
357        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        /// Get existing projects
380        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                    // NOTE: There is a bug happening when running e.g.:
386                    // `ispm packages --list --json | cat > test.txt; stat -c '%s' test.txt`
387                    // where the output to the pipe from ISPM stops after the size of the
388                    // PIPE_BUF. For now, we mitigate this by passing `--list-installed` only.
389                    .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    /// ISPM commands for platform management
400    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        /// Get existing platforms
410        pub fn list() -> Result<Platforms> {
411            Ok(from_slice(
412                &Command::new(ISPM_NAME)
413                    .arg(PLATFORMS_SUBCOMMAND)
414                    .arg(NON_INTERACTIVE_FLAG)
415                    // NOTE: There is a bug happening when running e.g.:
416                    // `ispm packages --list --json | cat > test.txt; stat -c '%s' test.txt`
417                    // where the output to the pipe from ISPM stops after the size of the
418                    // PIPE_BUF. For now, we mitigate this by passing `--list-installed` only.
419                    .arg("--list")
420                    .arg("--json")
421                    .check()
422                    .context(ISPM_NOT_FOUND_ERROR)?
423                    .stdout,
424            )?)
425        }
426    }
427
428    /// ISPM commands for settings management
429    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        /// Get the current ISPM configuration
439        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                // Fall back to reading the config from disk
451                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}