azure_functions/commands/
init.rs1use crate::{codegen::Function, commands::SyncExtensions, registry::Registry};
2use clap::{App, Arg, ArgMatches, SubCommand};
3use serde::Serialize;
4use serde_json::{json, to_string_pretty, Serializer};
5use std::env::{self, current_dir, current_exe};
6use std::fs;
7use std::path::{Path, PathBuf};
8
9pub struct Init<'a> {
10 pub script_root: PathBuf,
11 pub local_settings: Option<&'a str>,
12 pub host_settings: Option<&'a str>,
13 pub sync_extensions: bool,
14 pub no_debug_info: bool,
15 pub verbose: bool,
16}
17
18impl<'a> Init<'a> {
19 pub fn create_subcommand<'b>() -> App<'a, 'b> {
20 SubCommand::with_name("init")
21 .about("Initializes the Azure Functions application script root.")
22 .arg(
23 Arg::with_name("script_root")
24 .long("script-root")
25 .value_name("SCRIPT_ROOT")
26 .help("The script root directory to initialize the application in.")
27 .required(true),
28 )
29 .arg(
30 Arg::with_name("local_settings")
31 .long("local-settings")
32 .value_name("LOCAL_SETTINGS_FILE")
33 .help("The path to the local settings file to use. Defaults to the `local.settings.json` file in the directory containing `Cargo.toml`, if present.")
34 .validator(|v| {
35 if Path::new(&v).is_file() {
36 Ok(())
37 } else {
38 Err(format!("local settings file '{}' does not exist", v))
39 }
40 })
41 )
42 .arg(
43 Arg::with_name("host_settings")
44 .long("host-settings")
45 .value_name("HOST_SETTINGS_FILE")
46 .help("The path to the host settings file to use. Defaults to the `host.json` file in the directory containing `Cargo.toml`, if present.")
47 .validator(|v| {
48 if Path::new(&v).is_file() {
49 Ok(())
50 } else {
51 Err(format!("host settings file '{}' does not exist", v))
52 }
53 })
54 )
55 .arg(
56 Arg::with_name("sync_extensions")
57 .long("sync-extensions")
58 .short("s")
59 .help("Synchronize the Azure Function binding extensions.")
60 )
61 .arg(
62 Arg::with_name("no_debug_info")
63 .long("--no-debug-info")
64 .help("Do not copy debug information for the worker executable.")
65 )
66 .arg(
67 Arg::with_name("verbose")
68 .long("verbose")
69 .short("v")
70 .help("Use verbose output.")
71 )
72 }
73
74 pub fn execute(
75 &self,
76 registry: Registry<'static>,
77 extensions: &[(&str, &str)],
78 ) -> Result<(), String> {
79 self.create_script_root();
80
81 match self.get_local_path(&self.host_settings, "host.json") {
82 Some(path) => self.copy_host_settings_file(&path),
83 None => self.create_host_settings_file(),
84 };
85
86 match self.get_local_path(&self.local_settings, "local.settings.json") {
87 Some(path) => self.copy_local_settings_file(&path),
88 None => self.create_local_settings_file(),
89 };
90
91 let current_exe =
92 current_exe().expect("failed to determine the path to the current executable");
93
94 let worker_dir = self.create_worker_dir();
95 let worker_exe = worker_dir.join(current_exe.file_name().unwrap());
96
97 self.copy_worker_executable(¤t_exe, &worker_exe);
98
99 if !self.no_debug_info {
100 self.copy_worker_debug_info(¤t_exe, &worker_exe);
101 }
102
103 self.create_worker_config_file(&worker_dir, &worker_exe);
104
105 self.delete_existing_function_directories();
106
107 for (name, info) in registry.iter() {
108 let function_dir = self.create_function_directory(name);
109
110 let source_file = Init::get_source_file_path(
111 Path::new(
112 info.manifest_dir
113 .as_ref()
114 .expect("Functions should have a manifest directory.")
115 .as_ref(),
116 ),
117 Path::new(
118 info.file
119 .as_ref()
120 .expect("Functions should have a file.")
121 .as_ref(),
122 ),
123 );
124
125 self.copy_source_file(&function_dir, &source_file, name);
126 self.create_function_config_file(&function_dir, info);
127 }
128
129 if self.sync_extensions {
130 let command = SyncExtensions {
131 script_root: self.script_root.clone(),
132 verbose: self.verbose,
133 };
134 return command.execute(registry, extensions);
135 }
136
137 Ok(())
138 }
139
140 fn get_local_path(&self, path: &Option<&str>, filename: &str) -> Option<PathBuf> {
141 if let Some(path) = path {
142 return Some(path.into());
143 }
144
145 env::var("CARGO_MANIFEST_DIR")
146 .map(|dir| {
147 let path = PathBuf::from(dir).join(filename);
148 if path.is_file() {
149 Some(path)
150 } else {
151 None
152 }
153 })
154 .unwrap_or(None)
155 }
156
157 fn create_script_root(&self) {
158 if self.script_root.exists() {
159 if self.verbose {
160 println!(
161 "Using existing Azure Functions application at '{}'.",
162 self.script_root.display()
163 );
164 }
165 } else {
166 if self.verbose {
167 println!(
168 "Creating Azure Functions application at '{}'.",
169 self.script_root.display()
170 );
171 }
172
173 fs::create_dir_all(&self.script_root).unwrap_or_else(|e| {
174 panic!(
175 "failed to create Azure Functions application directory '{}': {}",
176 self.script_root.display(),
177 e
178 )
179 });
180 }
181 }
182
183 fn copy_host_settings_file(&self, local_host_file: &Path) {
184 let output_host_file = self.script_root.join("host.json");
185
186 if self.verbose {
187 println!(
188 "Copying host settings file '{}' to '{}'.",
189 local_host_file.display(),
190 output_host_file.display()
191 );
192 }
193
194 fs::copy(local_host_file, output_host_file).unwrap_or_else(|e| {
195 panic!(
196 "failed to copy host settings file '{}': {}",
197 local_host_file.display(),
198 e
199 )
200 });
201 }
202
203 fn create_host_settings_file(&self) {
204 let settings = self.script_root.join("host.json");
205
206 if self.verbose {
207 println!(
208 "Creating default host settings file '{}'.",
209 settings.display()
210 );
211 }
212
213 fs::write(
214 &settings,
215 to_string_pretty(&json!(
216 {
217 "version": "2.0",
218 "logging": {
219 "logLevel": {
220 "default": "Warning"
221 }
222 }
223 }))
224 .unwrap(),
225 )
226 .unwrap_or_else(|e| panic!("failed to create '{}': {}", settings.display(), e));
227 }
228
229 fn copy_local_settings_file(&self, local_settings_file: &Path) {
230 let output_settings = self.script_root.join("local.settings.json");
231
232 if self.verbose {
233 println!(
234 "Copying local settings file '{}' to '{}'.",
235 local_settings_file.display(),
236 output_settings.display()
237 );
238 }
239
240 fs::copy(local_settings_file, output_settings).unwrap_or_else(|e| {
241 panic!(
242 "failed to copy local settings file '{}': {}",
243 local_settings_file.display(),
244 e
245 )
246 });
247 }
248
249 fn create_local_settings_file(&self) {
250 let settings = self.script_root.join("local.settings.json");
251
252 if self.verbose {
253 println!(
254 "Creating default local settings file '{}'.",
255 settings.display()
256 );
257 }
258
259 fs::write(
260 &settings,
261 to_string_pretty(&json!(
262 {
263 "IsEncrypted": false,
264 "Values": {
265 "FUNCTIONS_WORKER_RUNTIME": "Rust",
266 "languageWorkers:workersDirectory": "workers"
267 },
268 "ConnectionStrings": {
269 }
270 }))
271 .unwrap(),
272 )
273 .unwrap_or_else(|e| panic!("failed to create '{}': {}", settings.display(), e));
274 }
275
276 fn create_worker_dir(&self) -> PathBuf {
277 let worker_dir = self.script_root.join("workers").join("rust");
278
279 if worker_dir.exists() {
280 fs::remove_dir_all(&worker_dir).unwrap_or_else(|e| {
281 panic!(
282 "failed to delete Rust worker directory '{}': {}",
283 worker_dir.display(),
284 e
285 )
286 });
287 }
288
289 if self.verbose {
290 println!("Creating worker directory '{}'.", worker_dir.display());
291 }
292
293 fs::create_dir_all(&worker_dir).unwrap_or_else(|e| {
294 panic!(
295 "failed to create directory for worker executable '{}': {}",
296 worker_dir.display(),
297 e
298 )
299 });
300
301 worker_dir
302 }
303
304 fn copy_worker_executable(&self, current_exe: &Path, worker_exe: &Path) {
305 if self.verbose {
306 println!(
307 "Copying current worker executable to '{}'.",
308 worker_exe.display()
309 );
310 }
311
312 fs::copy(current_exe, worker_exe).expect("Failed to copy worker executable");
313 }
314
315 #[cfg(target_os = "windows")]
316 fn copy_worker_debug_info(&self, current_exe: &Path, worker_exe: &Path) {
317 let current_pdb = current_exe.with_extension("pdb");
318 if !current_pdb.is_file() {
319 return;
320 }
321
322 let worker_pdb = worker_exe.with_extension("pdb");
323
324 if self.verbose {
325 println!(
326 "Copying worker debug information to '{}'.",
327 worker_pdb.display()
328 );
329 }
330
331 fs::copy(current_pdb, worker_pdb).expect("Failed to copy worker debug information");
332 }
333
334 #[cfg(target_os = "macos")]
335 fn copy_worker_debug_info(&self, current_exe: &Path, worker_exe: &Path) {
336 use fs_extra::dir;
337
338 let current_dsym = current_exe.with_extension("dSYM");
339 if !current_dsym.exists() {
340 return;
341 }
342
343 let worker_dsym = worker_exe.with_extension("dSYM");
344
345 if self.verbose {
346 println!(
347 "Copying worker debug information to '{}'.",
348 worker_dsym.display()
349 );
350 }
351
352 let mut options = dir::CopyOptions::new();
353 options.copy_inside = true;
354
355 dir::copy(current_dsym, worker_dsym, &options)
356 .expect("Failed to copy worker debug information");
357 }
358
359 #[cfg(target_os = "linux")]
360 fn copy_worker_debug_info(&self, _: &Path, _: &Path) {
361 }
363
364 fn create_worker_config_file(&self, worker_dir: &Path, worker_exe: &Path) {
365 let config = worker_dir.join("worker.config.json");
366 if config.exists() {
367 return;
368 }
369
370 if self.verbose {
371 println!("Creating worker config file '{}'.", config.display());
372 }
373
374 fs::write(
375 &config,
376 to_string_pretty(&json!(
377 {
378 "description":{
379 "language": "Rust",
380 "extensions": [".rs"],
381 "defaultExecutablePath": worker_exe.to_str().unwrap(),
382 "arguments": ["run"]
383 }
384 }))
385 .unwrap(),
386 )
387 .unwrap_or_else(|e| panic!("failed to create '{}': {}", config.display(), e));
388 }
389
390 fn delete_existing_function_directories(&self) {
391 for entry in fs::read_dir(&self.script_root).expect("failed to read script root directory")
392 {
393 let path = self
394 .script_root
395 .join(entry.expect("failed to read script root entry").path());
396 if !path.is_dir() || !Init::has_rust_files(&path) {
397 continue;
398 }
399
400 if self.verbose {
401 println!(
402 "Deleting existing Rust function directory '{}'.",
403 path.display()
404 );
405 }
406
407 fs::remove_dir_all(&path).unwrap_or_else(|e| {
408 panic!(
409 "failed to delete function directory '{}': {}",
410 path.display(),
411 e
412 )
413 });
414 }
415 }
416
417 fn create_function_directory(&self, function_name: &str) -> PathBuf {
418 let function_dir = self.script_root.join(function_name);
419
420 if self.verbose {
421 println!("Creating function directory '{}'.", function_dir.display());
422 }
423
424 fs::create_dir(&function_dir).unwrap_or_else(|e| {
425 panic!(
426 "failed to create function directory '{}': {}",
427 function_dir.display(),
428 e
429 )
430 });
431
432 function_dir
433 }
434
435 fn copy_source_file(&self, function_dir: &Path, source_file: &Path, function_name: &str) {
436 let destination_file = function_dir.join(
437 source_file
438 .file_name()
439 .expect("expected the source file to have a file name"),
440 );
441
442 if source_file.is_file() {
443 if self.verbose {
444 println!(
445 "Copying source file '{}' to '{}' for Azure Function '{}'.",
446 source_file.display(),
447 destination_file.display(),
448 function_name
449 );
450 }
451
452 fs::copy(&source_file, destination_file).unwrap_or_else(|e| {
453 panic!(
454 "failed to copy source file '{}': {}",
455 source_file.display(),
456 e
457 )
458 });
459 } else {
460 if self.verbose {
461 println!(
462 "Creating empty source file '{}' for Azure Function '{}'.",
463 destination_file.display(),
464 function_name
465 );
466 }
467
468 fs::write(
469 &destination_file,
470 "// This file is intentionally empty.\n\
471 // The original source file was not available when the Functions Application was initialized.\n"
472 ).unwrap_or_else(|e| panic!("failed to create '{}': {}", destination_file.display(), e));
473 }
474 }
475
476 fn create_function_config_file(&self, function_dir: &Path, info: &'static Function) {
477 let function_json = function_dir.join("function.json");
478
479 if self.verbose {
480 println!(
481 "Creating function configuration file '{}' for Azure Function '{}'.",
482 function_json.display(),
483 info.name
484 );
485 }
486
487 let mut output = fs::File::create(&function_json)
488 .unwrap_or_else(|e| panic!("failed to create '{}': {}", function_json.display(), e));
489
490 info.serialize(&mut Serializer::pretty(&mut output))
491 .unwrap_or_else(|e| {
492 panic!(
493 "failed to serialize metadata for function '{}': {}",
494 info.name, e
495 )
496 });
497 }
498
499 fn get_source_file_path(manifest_dir: &Path, file: &Path) -> PathBuf {
504 let mut manifest_dir = Path::new(manifest_dir);
505 for component in file.components() {
506 if component.as_os_str() == "src" {
507 break;
508 }
509 manifest_dir = manifest_dir
510 .parent()
511 .expect("expected another parent for the manifest directory");
512 }
513
514 manifest_dir.join(file)
515 }
516
517 fn has_rust_files(directory: &Path) -> bool {
518 fs::read_dir(directory)
519 .unwrap_or_else(|e| panic!("failed to read directory '{}': {}", directory.display(), e))
520 .any(|p| match p {
521 Ok(p) => {
522 let p = p.path();
523 p.is_file() && p.extension().map(|x| x == "rs").unwrap_or(false)
524 }
525 _ => false,
526 })
527 }
528}
529
530impl<'a> From<&'a ArgMatches<'a>> for Init<'a> {
531 fn from(args: &'a ArgMatches<'a>) -> Self {
532 Init {
533 script_root: current_dir()
534 .expect("failed to get current directory")
535 .join(
536 args.value_of("script_root")
537 .expect("A script root is required."),
538 ),
539 local_settings: args.value_of("local_settings"),
540 host_settings: args.value_of("host_settings"),
541 sync_extensions: args.is_present("sync_extensions"),
542 no_debug_info: args.is_present("no_debug_info"),
543 verbose: args.is_present("verbose"),
544 }
545 }
546}