azure-functions 0.11.0

Azure Functions for Rust
Documentation
use crate::registry::Registry;
use clap::{App, Arg, ArgMatches, SubCommand};
use std::{
    collections::HashMap,
    env::current_dir,
    fs,
    path::{Path, PathBuf},
    process::Command,
};
use tempfile::TempDir;
use xml::{
    writer::{EventWriter, XmlEvent},
    EmitterConfig,
};

pub struct SyncExtensions {
    pub script_root: PathBuf,
    pub verbose: bool,
}

impl SyncExtensions {
    pub fn create_subcommand<'a, 'b>() -> App<'a, 'b> {
        SubCommand::with_name("sync-extensions")
            .about("Synchronizes the Azure Function binding extensions used by the worker.")
            .arg(
                Arg::with_name("script_root")
                    .long("script-root")
                    .value_name("SCRIPT_ROOT")
                    .help("The script root to synchronize the binding extensions for.")
                    .required(true),
            )
            .arg(
                Arg::with_name("verbose")
                    .long("verbose")
                    .short("v")
                    .help("Use verbose output."),
            )
    }

    pub fn execute(
        &self,
        registry: Registry<'static>,
        extensions: &[(&str, &str)],
    ) -> Result<(), String> {
        let extensions = registry.build_extensions_map(extensions);
        if extensions.is_empty() {
            if self.verbose {
                println!("No binding extensions are needed.");
            }
            return Ok(());
        }

        let temp_dir = TempDir::new().expect("failed to create temporary directory");
        let extensions_project_path = temp_dir.path().join("extensions.csproj");
        let metadata_project_path = temp_dir.path().join("metadata.csproj");

        self.write_extensions_project_file(&extensions_project_path, &extensions);
        Self::write_generator_project_file(&metadata_project_path);

        if self.verbose {
            println!("Restoring extension assemblies...");
        }

        let status = Command::new("dotnet")
            .args(&[
                "publish",
                "-c",
                "Release",
                "-o",
                self.script_root.join("bin").to_str().unwrap(),
                "/v:q",
                "/nologo",
                extensions_project_path.to_str().unwrap(),
            ])
            .current_dir(temp_dir.path())
            .status()
            .unwrap_or_else(|e| panic!("failed to spawn dotnet: {}", e));

        if !status.success() {
            panic!(
                "failed to restore extensions: dotnet returned non-zero exit code {}.",
                status.code().unwrap()
            );
        }

        if self.verbose {
            println!("Generating extension metadata...");
        }

        let status = Command::new("dotnet")
            .args(&[
                "msbuild",
                "/t:_GenerateFunctionsExtensionsMetadataPostPublish",
                "/v:q",
                "/nologo",
                "/restore",
                "-p:Configuration=Release",
                &format!("-p:PublishDir={}/", self.script_root.to_str().unwrap()),
                metadata_project_path.to_str().unwrap(),
            ])
            .current_dir(temp_dir.path())
            .status()
            .unwrap_or_else(|e| panic!("failed to spawn dotnet: {}", e));

        if !status.success() {
            panic!(
                "failed to generate extension metadata: dotnet returned non-zero exit code {}.",
                status.code().unwrap()
            );
        }

        Ok(())
    }

    fn write_property(writer: &mut xml::EventWriter<&mut fs::File>, name: &str, value: &str) {
        writer.write(XmlEvent::start_element(name)).unwrap();
        writer.write(XmlEvent::characters(value)).unwrap();
        writer.write(XmlEvent::end_element()).unwrap();
    }

    fn write_extensions_project_file(&self, path: &Path, extensions: &HashMap<String, String>) {
        let mut project_file =
            fs::File::create(path).expect("Failed to create extensions project file.");

        let mut writer = EmitterConfig::new()
            .perform_indent(true)
            .create_writer(&mut project_file);

        writer
            .write(XmlEvent::start_element("Project").attr("Sdk", "Microsoft.NET.Sdk"))
            .unwrap();

        writer
            .write(XmlEvent::start_element("PropertyGroup"))
            .unwrap();

        Self::write_property(&mut writer, "TargetFramework", "netstandard2.0");
        Self::write_property(&mut writer, "CopyBuildOutputToPublishDirectory", "false");
        Self::write_property(&mut writer, "CopyOutputSymbolsToPublishDirectory", "false");
        Self::write_property(&mut writer, "GenerateDependencyFile", "false");

        writer.write(XmlEvent::end_element()).unwrap();

        writer.write(XmlEvent::start_element("ItemGroup")).unwrap();

        for (name, version) in extensions {
            if self.verbose {
                println!("Synchronizing version {} of extension '{}'.", version, name);
            }
            Self::write_package_reference(&mut writer, name, version, None);
        }

        writer.write(XmlEvent::end_element()).unwrap();
        writer.write(XmlEvent::end_element()).unwrap();
    }

    fn write_package_reference(
        writer: &mut EventWriter<&mut fs::File>,
        package: &str,
        version: &str,
        private_assets: Option<&str>,
    ) {
        let mut element = XmlEvent::start_element("PackageReference")
            .attr("Include", package)
            .attr("Version", version);

        if let Some(private_assets) = private_assets {
            element = element.attr("PrivateAssets", private_assets);
        }

        writer.write(element).unwrap();
        writer.write(XmlEvent::end_element()).unwrap();
    }

    fn write_generator_project_file(path: &Path) {
        let mut project_file =
            fs::File::create(path).expect("Failed to create generator project file.");

        let mut writer = EmitterConfig::new()
            .perform_indent(true)
            .create_writer(&mut project_file);

        writer
            .write(XmlEvent::start_element("Project").attr("Sdk", "Microsoft.NET.Sdk"))
            .unwrap();

        writer
            .write(XmlEvent::start_element("PropertyGroup"))
            .unwrap();

        Self::write_property(&mut writer, "TargetFramework", "netstandard2.0");

        writer.write(XmlEvent::end_element()).unwrap();

        writer.write(XmlEvent::start_element("ItemGroup")).unwrap();

        Self::write_package_reference(
            &mut writer,
            "Microsoft.Azure.WebJobs.Script.ExtensionsMetadataGenerator",
            "1.0.1",
            Some("all"),
        );

        writer.write(XmlEvent::end_element()).unwrap();
        writer.write(XmlEvent::end_element()).unwrap();
    }
}

impl<'a> From<&ArgMatches<'a>> for SyncExtensions {
    fn from(args: &ArgMatches<'a>) -> Self {
        SyncExtensions {
            script_root: current_dir()
                .expect("failed to get current directory")
                .join(
                    args.value_of("script_root")
                        .expect("A script root is required."),
                ),
            verbose: args.is_present("verbose"),
        }
    }
}