azure_functions/commands/
sync_extensions.rs

1use crate::registry::Registry;
2use clap::{App, Arg, ArgMatches, SubCommand};
3use std::{
4    collections::HashMap,
5    env::current_dir,
6    fs,
7    path::{Path, PathBuf},
8    process::Command,
9};
10use tempfile::TempDir;
11use xml::{
12    writer::{EventWriter, XmlEvent},
13    EmitterConfig,
14};
15
16pub struct SyncExtensions {
17    pub script_root: PathBuf,
18    pub verbose: bool,
19}
20
21impl SyncExtensions {
22    pub fn create_subcommand<'a, 'b>() -> App<'a, 'b> {
23        SubCommand::with_name("sync-extensions")
24            .about("Synchronizes the Azure Function binding extensions used by the worker.")
25            .arg(
26                Arg::with_name("script_root")
27                    .long("script-root")
28                    .value_name("SCRIPT_ROOT")
29                    .help("The script root to synchronize the binding extensions for.")
30                    .required(true),
31            )
32            .arg(
33                Arg::with_name("verbose")
34                    .long("verbose")
35                    .short("v")
36                    .help("Use verbose output."),
37            )
38    }
39
40    pub fn execute(
41        &self,
42        registry: Registry<'static>,
43        extensions: &[(&str, &str)],
44    ) -> Result<(), String> {
45        let extensions = registry.build_extensions_map(extensions);
46        if extensions.is_empty() {
47            if self.verbose {
48                println!("No binding extensions are needed.");
49            }
50            return Ok(());
51        }
52
53        let temp_dir = TempDir::new().expect("failed to create temporary directory");
54        let extensions_project_path = temp_dir.path().join("extensions.csproj");
55        let metadata_project_path = temp_dir.path().join("metadata.csproj");
56
57        self.write_extensions_project_file(&extensions_project_path, &extensions);
58        Self::write_generator_project_file(&metadata_project_path);
59
60        if self.verbose {
61            println!("Restoring extension assemblies...");
62        }
63
64        let status = Command::new("dotnet")
65            .args(&[
66                "publish",
67                "-c",
68                "Release",
69                "-o",
70                self.script_root.join("bin").to_str().unwrap(),
71                "/v:q",
72                "/nologo",
73                extensions_project_path.to_str().unwrap(),
74            ])
75            .current_dir(temp_dir.path())
76            .status()
77            .unwrap_or_else(|e| panic!("failed to spawn dotnet: {}", e));
78
79        if !status.success() {
80            panic!(
81                "failed to restore extensions: dotnet returned non-zero exit code {}.",
82                status.code().unwrap()
83            );
84        }
85
86        if self.verbose {
87            println!("Generating extension metadata...");
88        }
89
90        let status = Command::new("dotnet")
91            .args(&[
92                "msbuild",
93                "/t:_GenerateFunctionsExtensionsMetadataPostPublish",
94                "/v:q",
95                "/nologo",
96                "/restore",
97                "-p:Configuration=Release",
98                &format!("-p:PublishDir={}/", self.script_root.to_str().unwrap()),
99                metadata_project_path.to_str().unwrap(),
100            ])
101            .current_dir(temp_dir.path())
102            .status()
103            .unwrap_or_else(|e| panic!("failed to spawn dotnet: {}", e));
104
105        if !status.success() {
106            panic!(
107                "failed to generate extension metadata: dotnet returned non-zero exit code {}.",
108                status.code().unwrap()
109            );
110        }
111
112        Ok(())
113    }
114
115    fn write_property(writer: &mut xml::EventWriter<&mut fs::File>, name: &str, value: &str) {
116        writer.write(XmlEvent::start_element(name)).unwrap();
117        writer.write(XmlEvent::characters(value)).unwrap();
118        writer.write(XmlEvent::end_element()).unwrap();
119    }
120
121    fn write_extensions_project_file(&self, path: &Path, extensions: &HashMap<String, String>) {
122        let mut project_file =
123            fs::File::create(path).expect("Failed to create extensions project file.");
124
125        let mut writer = EmitterConfig::new()
126            .perform_indent(true)
127            .create_writer(&mut project_file);
128
129        writer
130            .write(XmlEvent::start_element("Project").attr("Sdk", "Microsoft.NET.Sdk"))
131            .unwrap();
132
133        writer
134            .write(XmlEvent::start_element("PropertyGroup"))
135            .unwrap();
136
137        Self::write_property(&mut writer, "TargetFramework", "netstandard2.0");
138        Self::write_property(&mut writer, "CopyBuildOutputToPublishDirectory", "false");
139        Self::write_property(&mut writer, "CopyOutputSymbolsToPublishDirectory", "false");
140        Self::write_property(&mut writer, "GenerateDependencyFile", "false");
141
142        writer.write(XmlEvent::end_element()).unwrap();
143
144        writer.write(XmlEvent::start_element("ItemGroup")).unwrap();
145
146        for (name, version) in extensions {
147            if self.verbose {
148                println!("Synchronizing version {} of extension '{}'.", version, name);
149            }
150            Self::write_package_reference(&mut writer, name, version, None);
151        }
152
153        writer.write(XmlEvent::end_element()).unwrap();
154        writer.write(XmlEvent::end_element()).unwrap();
155    }
156
157    fn write_package_reference(
158        writer: &mut EventWriter<&mut fs::File>,
159        package: &str,
160        version: &str,
161        private_assets: Option<&str>,
162    ) {
163        let mut element = XmlEvent::start_element("PackageReference")
164            .attr("Include", package)
165            .attr("Version", version);
166
167        if let Some(private_assets) = private_assets {
168            element = element.attr("PrivateAssets", private_assets);
169        }
170
171        writer.write(element).unwrap();
172        writer.write(XmlEvent::end_element()).unwrap();
173    }
174
175    fn write_generator_project_file(path: &Path) {
176        let mut project_file =
177            fs::File::create(path).expect("Failed to create generator project file.");
178
179        let mut writer = EmitterConfig::new()
180            .perform_indent(true)
181            .create_writer(&mut project_file);
182
183        writer
184            .write(XmlEvent::start_element("Project").attr("Sdk", "Microsoft.NET.Sdk"))
185            .unwrap();
186
187        writer
188            .write(XmlEvent::start_element("PropertyGroup"))
189            .unwrap();
190
191        Self::write_property(&mut writer, "TargetFramework", "netstandard2.0");
192
193        writer.write(XmlEvent::end_element()).unwrap();
194
195        writer.write(XmlEvent::start_element("ItemGroup")).unwrap();
196
197        Self::write_package_reference(
198            &mut writer,
199            "Microsoft.Azure.WebJobs.Script.ExtensionsMetadataGenerator",
200            "1.0.1",
201            Some("all"),
202        );
203
204        writer.write(XmlEvent::end_element()).unwrap();
205        writer.write(XmlEvent::end_element()).unwrap();
206    }
207}
208
209impl<'a> From<&ArgMatches<'a>> for SyncExtensions {
210    fn from(args: &ArgMatches<'a>) -> Self {
211        SyncExtensions {
212            script_root: current_dir()
213                .expect("failed to get current directory")
214                .join(
215                    args.value_of("script_root")
216                        .expect("A script root is required."),
217                ),
218            verbose: args.is_present("verbose"),
219        }
220    }
221}