picodata-pike 2.8.0

Cargo plugin for Picodata plugin development
Documentation
use crate::commands::lib::{cargo_build, BuildType, LIB_EXT};
use anyhow::{bail, Context, Result};
use flate2::write::GzEncoder;
use flate2::Compression;
use serde::Deserialize;
use std::fs::File;
use std::path::{Path, PathBuf};
use std::{env, fs};
use tar::Builder;
use toml::Value;

#[derive(Deserialize)]
struct PackageInfo {
    name: String,
    version: String,
}

#[derive(Deserialize)]
struct CargoManifest {
    package: PackageInfo,
}

pub fn cmd(pack_debug: bool, target_dir: &PathBuf, pluging_path: &PathBuf) -> Result<()> {
    let root_dir = env::current_dir()?.join(pluging_path);

    let build_dir = if pack_debug {
        cargo_build(BuildType::Debug, target_dir, pluging_path)
            .context("building release version of plugin")?;
        Path::new(&root_dir).join(target_dir).join("debug")
    } else {
        cargo_build(BuildType::Release, target_dir, pluging_path)
            .context("building debug version of plugin")?;
        Path::new(&root_dir).join(target_dir).join("release")
    };

    let plugin_dir = root_dir.clone();

    let cargo_toml_path = root_dir.join("Cargo.toml");
    let cargo_toml_content = fs::read_to_string(&cargo_toml_path).context(format!(
        "Failed to read Cargo.toml in {}",
        &cargo_toml_path.display()
    ))?;

    let parsed_toml: Value = cargo_toml_content
        .parse()
        .context("Failed to parse Cargo.toml")?;

    if let Some(workspace) = parsed_toml.get("workspace") {
        if let Some(members) = workspace.get("members") {
            if let Some(members_array) = members.as_array() {
                for member in members_array {
                    let member_str = member.as_str();
                    if member_str.is_none() {
                        continue;
                    }

                    if !root_dir
                        .join(member_str.unwrap())
                        .join("manifest.yaml.template")
                        .exists()
                    {
                        continue;
                    }

                    create_plugin_archive(&build_dir, &root_dir.join(member_str.unwrap()))?;
                }
            }
        }

        return Ok(());
    }

    create_plugin_archive(&build_dir, &plugin_dir)
}

fn create_plugin_archive(build_dir: &Path, plugin_dir: &Path) -> Result<()> {
    let plugin_version = get_latest_plugin_version(plugin_dir)?;

    let cargo_manifest: CargoManifest = toml::from_str(
        &fs::read_to_string(plugin_dir.join("Cargo.toml")).context("failed to read Cargo.toml")?,
    )
    .context("failed to parse Cargo.toml")?;

    let package_name = cargo_manifest.package.name;
    let normalized_package_name = package_name.replace('-', "_");

    let plugin_build_dir = build_dir.join(&package_name).join(&plugin_version);

    let root_in_zip = Path::new(&package_name).join(plugin_version);

    let compressed_file_path = format!(
        "{}/{package_name}-{}.tar.gz",
        build_dir.display(),
        cargo_manifest.package.version
    );
    let compressed_file =
        File::create(compressed_file_path).context("failed to pack the plugin")?;

    let mut encoder = GzEncoder::new(compressed_file, Compression::best());

    let lib_name = format!("lib{normalized_package_name}.{LIB_EXT}");

    {
        let mut tarball = Builder::new(&mut encoder);

        archive_if_exists(
            &root_in_zip,
            &plugin_build_dir.join(&lib_name),
            &mut tarball,
        )?;
        archive_if_exists(
            &root_in_zip,
            &plugin_build_dir.join("manifest.yaml"),
            &mut tarball,
        )?;
        archive_if_exists(
            &root_in_zip,
            &plugin_build_dir.join("migrations"),
            &mut tarball,
        )?;

        let assets_path = &plugin_build_dir.join("assets");
        // no need to notify user if there is no assets folder
        if assets_path.exists() {
            for entry in fs::read_dir(assets_path)? {
                let entry = entry?;
                let entry_name = entry.file_name();
                archive_if_exists(&root_in_zip, &assets_path.join(entry_name), &mut tarball)?;
            }
        }
    }

    encoder.finish()?;

    Ok(())
}

fn archive_if_exists(
    root_in_zip: &Path,
    file_path: &Path,
    tarball: &mut Builder<&mut GzEncoder<File>>,
) -> Result<()> {
    if !file_path.exists() {
        log::info!(
            "Couldn't find {} while packing plugin - skipping.",
            file_path.display()
        );

        return Ok(());
    }

    let archived_file_name = root_in_zip.join(file_path.file_name().unwrap());

    if file_path.is_dir() {
        tarball
            .append_dir_all(archived_file_name, file_path)
            .context(format!(
                "failed to append directory: {} to archive",
                file_path.display()
            ))?;
    } else {
        let mut opened_file = File::open(file_path)
            .context(format!("failed to open file {}", &file_path.display()))?;

        tarball
            .append_file(archived_file_name, &mut opened_file)
            .context(format!(
                "failed to append file: {} to archive",
                file_path.display()
            ))?;
    }

    Ok(())
}

fn get_latest_plugin_version(plugin_dir: &Path) -> Result<String> {
    let cargo_toml =
        fs::read_to_string(plugin_dir.join("Cargo.toml")).expect("Failed to read Cargo.toml");

    let parsed: toml::Value = toml::de::from_str(&cargo_toml).expect("Failed to parse TOML");

    if let Some(package) = parsed.get("package") {
        if let Some(version) = package.get("version") {
            return Ok(version
                .to_string()
                .strip_prefix("\"")
                .unwrap()
                .strip_suffix("\"")
                .unwrap()
                .to_string());
        }
        bail!("Couldn't find version in plugin Cargo.toml");
    }

    bail!(
        "Couldn't resolve plugin version from Cargo.toml at {}",
        plugin_dir.display()
    )
}