azure_functions/commands/
init.rs

1use 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(&current_exe, &worker_exe);
98
99        if !self.no_debug_info {
100            self.copy_worker_debug_info(&current_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        // No-op
362    }
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    // This is a workaround to the issue that `file!` expands to be workspace-relative
500    // and cargo does not have an environment variable for the workspace directory.
501    // Thus, this walks up the manifest directory until it hits "src" in the file's path.
502    // This function is sensitive to cargo and rustc changes.
503    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}