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, 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
28/// Minimal implementation of internal ISPM functionality to use it externally
29pub struct Internal;
30
31impl Internal {
32    // NOTE: Can be found in package.json in extracted ispm application
33    const PRODUCT_NAME: &'static str = "Intel Simics Package Manager";
34
35    // NOTE: Can be found in `AppInfo` class in extracted ispm application
36    const CFG_FILENAME: &'static str = "simics-package-manager.cfg";
37
38    // NOTE: Can be found in `constructAppDataPath` in extracted ispm application
39    /// Retrieve the path to the directory containing ISPM's application data, in particular the
40    /// configuration file.
41    fn app_data_path() -> Result<PathBuf> {
42        #[allow(deprecated)]
43        // NOTE: Use of deprecated home_dir is ok because the "incorrect" windows behavior is actually
44        // correct for SIMICS' use case.
45        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        // This comes from the ispm source, it's hardcoded there and we hardcode it here
52        return Ok(home_dir
53            .join("AppData")
54            .join("Local")
55            .join(Self::PRODUCT_NAME));
56    }
57
58    // NOTE: Can be found in `getCfgFileName` in extracted ispm application
59    /// Retrieve the path to the ISPM configuration file
60    pub fn cfg_file_path() -> Result<PathBuf> {
61        Ok(Self::app_data_path()?.join(Self::CFG_FILENAME))
62    }
63
64    /// Returns whether this is an internal release of ISPM
65    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
75/// An implementor can convert itself into a list of command-line arguments
76pub trait ToArgs {
77    /// Convert this implementor into a list of command-line arguments
78    fn to_args(&self) -> Vec<String>;
79}
80
81/// Wrappers for ISPM commands
82pub 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    /// Global ISPM options
91    pub struct GlobalOptions {
92        #[builder(default, setter(into))]
93        /// A package repo to use when installing packages
94        pub package_repo: Vec<String>,
95        #[builder(default, setter(into, strip_option))]
96        /// A directory to install packages into, overriding global configurations
97        pub install_dir: Option<PathBuf>,
98        #[builder(default, setter(into, strip_option))]
99        /// An HTTPS proxy URL to use
100        pub https_proxy: Option<String>,
101        #[builder(default, setter(into, strip_option))]
102        /// A no-proxy string of addresses not to use the proxy for, e.g. "*.intel.com,127.0.0.1"
103        pub no_proxy: Option<String>,
104        #[builder(default = true)]
105        /// Whether this command should be run in non-interactive mode.
106        pub non_interactive: bool,
107        #[builder(default = false)]
108        /// Whether insecure packages should be trusted. This should be set to true when
109        /// installing an un-signed local package
110        pub trust_insecure_packages: bool,
111        #[builder(default, setter(into, strip_option))]
112        /// A path to an override configuration file
113        pub config_file: Option<PathBuf>,
114        #[builder(default = false)]
115        /// Whether the configuration file should not be used for this command
116        pub no_config_file: bool,
117        #[builder(default, setter(into, strip_option))]
118        /// A different temporary directory to use
119        pub temp_dir: Option<PathBuf>,
120        #[builder(default, setter(into, strip_option))]
121        /// An authentication file to use for this command
122        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    /// ISPM commands for package management
191    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        /// Get the currently installed and available packages
207        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                    // NOTE: There is a bug happening when running e.g.:
213                    // `ispm packages --list --json | cat > test.txt; stat -c '%s' test.txt`
214                    // where the output to the pipe from ISPM stops after the size of the
215                    // PIPE_BUF. For now, we mitigate this by passing `--list-installed` only.
216                    .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        /// Options that can be set when installing one or more packages
230        pub struct InstallOptions {
231            #[builder(default, setter(into))]
232            /// Packages to install by number/version
233            pub packages: HashSet<ProjectPackage>,
234            #[builder(default, setter(into))]
235            /// Packages to install by local path
236            pub package_paths: Vec<PathBuf>,
237            #[builder(default)]
238            /// Global ispm options
239            pub global: GlobalOptions,
240            #[builder(default = false)]
241            /// Whether to install all packages
242            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        /// Install a package or set of packages, executing the ispm command
263        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        /// Options that can be set when uninstalling one or more packages
274        pub struct UninstallOptions {
275            #[builder(default, setter(into))]
276            /// Packages to install by number/version
277            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        /// Uninstall a package or set of packages, executing the ispm command
293        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    /// ISPM commands for project management
304    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        /// Options that can be set when creating a project
323        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        /// Create a project
348        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        /// Get existing projects
368        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                    // NOTE: There is a bug happening when running e.g.:
374                    // `ispm packages --list --json | cat > test.txt; stat -c '%s' test.txt`
375                    // where the output to the pipe from ISPM stops after the size of the
376                    // PIPE_BUF. For now, we mitigate this by passing `--list-installed` only.
377                    .arg("--list")
378                    .arg("--json")
379                    .args(options.to_args())
380                    .check()?
381                    .stdout,
382            )?)
383        }
384    }
385
386    /// ISPM commands for platform management
387    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        /// Get existing platforms
397        pub fn list() -> Result<Platforms> {
398            Ok(from_slice(
399                &Command::new(ISPM_NAME)
400                    .arg(PLATFORMS_SUBCOMMAND)
401                    .arg(NON_INTERACTIVE_FLAG)
402                    // NOTE: There is a bug happening when running e.g.:
403                    // `ispm packages --list --json | cat > test.txt; stat -c '%s' test.txt`
404                    // where the output to the pipe from ISPM stops after the size of the
405                    // PIPE_BUF. For now, we mitigate this by passing `--list-installed` only.
406                    .arg("--list")
407                    .arg("--json")
408                    .check()?
409                    .stdout,
410            )?)
411        }
412    }
413
414    /// ISPM commands for settings management
415    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        /// Get the current ISPM configuration
425        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                // Fall back to reading the config from disk
436                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}