azure_functions/commands/
sync_extensions.rs1use 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}