flowey_lib_common/
nuget_install_package.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Install a nuget package
5
6use crate::download_nuget_exe::NugetInstallPlatform;
7use flowey::node::prelude::*;
8use std::collections::BTreeMap;
9use std::fmt::Write as _;
10
11#[derive(Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone, Debug)]
12pub struct NugetPackage {
13    pub id: String,
14    pub version: String,
15}
16
17flowey_request! {
18    pub enum Request {
19        /// A bundle of packages to install in one nuget invocation
20        Install {
21            /// Path to a nuget.config file
22            nuget_config_file: ReadVar<PathBuf>,
23            /// A list of nuget packages to install, and outvars denoting where they
24            /// were extracted to.
25            packages: Vec<(ReadVar<NugetPackage>, WriteVar<PathBuf>)>,
26            /// Directory to install the packages into.
27            install_dir: ReadVar<PathBuf>,
28            /// Side effects that must have run before installing these packages.
29            ///
30            /// e.g: requiring that a nuget credentials manager has been installed
31            pre_install_side_effects: Vec<ReadVar<SideEffect>>,
32        },
33        /// Whether to pass `-NonInteractive` to `nuget install`
34        LocalOnlyInteractive(bool),
35    }
36}
37
38struct InstallRequest {
39    nuget_config_file: ReadVar<PathBuf>,
40    packages: Vec<(ReadVar<NugetPackage>, WriteVar<PathBuf>)>,
41    install_dir: ReadVar<PathBuf>,
42    pre_install_side_effects: Vec<ReadVar<SideEffect>>,
43}
44
45new_flow_node!(struct Node);
46
47impl FlowNode for Node {
48    type Request = Request;
49
50    fn imports(ctx: &mut ImportCtx<'_>) {
51        ctx.import::<super::install_nuget_azure_credential_provider::Node>();
52        ctx.import::<super::download_nuget_exe::Node>();
53    }
54
55    fn emit(requests: Vec<Self::Request>, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()> {
56        let mut interactive = None;
57        let mut install = Vec::new();
58
59        for request in requests {
60            match request {
61                Request::LocalOnlyInteractive(v) => {
62                    same_across_all_reqs("LocalOnlyInteractive", &mut interactive, v)?
63                }
64
65                Request::Install {
66                    packages,
67                    nuget_config_file,
68                    install_dir,
69                    pre_install_side_effects,
70                } => install.push(InstallRequest {
71                    packages,
72                    nuget_config_file,
73                    install_dir,
74                    pre_install_side_effects,
75                }),
76            }
77        }
78
79        let interactive = if matches!(ctx.backend(), FlowBackend::Ado | FlowBackend::Github) {
80            if interactive.is_some() {
81                anyhow::bail!("can only use `LocalOnlyInteractive` when using the Local backend");
82            }
83            false
84        } else if matches!(ctx.backend(), FlowBackend::Local) {
85            interactive.ok_or(anyhow::anyhow!(
86                "Missing essential request: LocalOnlyInteractive",
87            ))?
88        } else {
89            anyhow::bail!("unsupported backend")
90        };
91
92        // -- end of req processing -- //
93
94        if install.is_empty() {
95            return Ok(());
96        }
97
98        // need nuget to be installed
99        let nuget_bin = ctx.reqv(super::download_nuget_exe::Request::NugetBin);
100
101        let nuget_config_platform =
102            ctx.reqv(super::download_nuget_exe::Request::NugetInstallPlatform);
103
104        for InstallRequest {
105            packages,
106            nuget_config_file,
107            install_dir,
108            pre_install_side_effects,
109        } in install
110        {
111            ctx.emit_rust_step("restore nuget packages", |ctx| {
112                let nuget_bin = nuget_bin.clone().claim(ctx);
113                let nuget_config_platform = nuget_config_platform.clone().claim(ctx);
114                let install_dir = install_dir.claim(ctx);
115                pre_install_side_effects.claim(ctx);
116
117                let packages = packages
118                    .into_iter()
119                    .map(|(a, b)| (a.claim(ctx), b.claim(ctx)))
120                    .collect::<Vec<_>>();
121                let nuget_config_file = nuget_config_file.claim(ctx);
122
123                move |rt| {
124                    let nuget_bin = rt.read(nuget_bin);
125                    let nuget_config_platform = rt.read(nuget_config_platform);
126                    let nuget_config_file = rt.read(nuget_config_file);
127                    let install_dir = rt.read(install_dir);
128
129                    let packages = {
130                        let mut pkgmap: BTreeMap<_, Vec<_>> = BTreeMap::new();
131                        for (package, var) in packages {
132                            pkgmap.entry(rt.read(package)).or_default().push(var);
133                        }
134                        pkgmap
135                    };
136
137                    // for whatever reason, unlike most other package managers,
138                    // nuget doesn't actually let you pass a list of arbitrary
139                    // packages to restore directly from the CLI. Unless you
140                    // want to constantly re-invoke nuget.exe, you're forced to
141                    // maintain a packages.config.
142                    //
143                    // To work around this, simply generate a packages.config on
144                    // the fly.
145                    let packages_config = {
146                        let mut packages_config = String::new();
147                        let _ =
148                            writeln!(packages_config, r#"<?xml version="1.0" encoding="utf-8"?>"#);
149                        let _ = writeln!(packages_config, r#"<packages>"#);
150                        for NugetPackage { id, version } in packages.keys() {
151                            let _ = writeln!(
152                                packages_config,
153                                r#"  <package id="{}" version="{}" />"#,
154                                id, version
155                            );
156                        }
157                        let _ = writeln!(packages_config, r#"</packages>"#);
158                        packages_config
159                    };
160
161                    log::debug!("generated package.config:\n{}", packages_config);
162
163                    let packages_config_filepath = PathBuf::from("./packages.config");
164
165                    fs_err::write(&packages_config_filepath, packages_config)?;
166
167                    // If we're crossing the WSL boundary we need to translate our config paths.
168                    let (packages_config_filepath, config_filepath) =
169                        if crate::_util::running_in_wsl(rt)
170                            && matches!(nuget_config_platform, NugetInstallPlatform::Windows)
171                        {
172                            (
173                                crate::_util::wslpath::linux_to_win(&packages_config_filepath),
174                                crate::_util::wslpath::linux_to_win(&nuget_config_file),
175                            )
176                        } else {
177                            (packages_config_filepath, nuget_config_file)
178                        };
179
180                    // now, run the nuget install command
181                    let non_interactive = (!interactive).then_some("-NonInteractive");
182
183                    // FUTURE: add checks to avoid having to invoke
184                    // nuget at all (a-la the "expected_hashes" in the
185                    // old ci/restore.sh)
186                    let sh = xshell::Shell::new()?;
187                    xshell::cmd!(
188                        sh,
189                        "{nuget_bin}
190                                    install
191                                    {non_interactive...}
192                                    -ExcludeVersion
193                                    -OutputDirectory {install_dir}
194                                    -ConfigFile {config_filepath}
195                                    {packages_config_filepath}
196                                "
197                    )
198                    .run()?;
199
200                    for (package, package_out_dir) in packages {
201                        for var in package_out_dir {
202                            rt.write(var, &install_dir.join(&package.id).absolute()?);
203                        }
204                    }
205
206                    Ok(())
207                }
208            });
209        }
210
211        Ok(())
212    }
213}