buffrs 0.13.2

Modern protobuf package management
Documentation
// Copyright 2023 Helsing GmbH
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use clap::{Parser, Subcommand};
use miette::{WrapErr, miette};
use semver::Version;

use buffrs::{
    command,
    logs::BuffrsEventFormatter,
    manifest::{MANIFEST_FILE, Manifest},
    operations::install::NetworkMode,
    package::{PackageName, PackageStore, PackageType},
    registry::RegistryUri,
};

#[derive(Parser)]
#[command(author, version, about, long_about)]
#[command(propagate_version = true)]
struct Cli {
    /// Enable verbose logging
    #[clap(short, long, env = "BUFFRS_VERBOSE", default_value_t = false)]
    verbose: bool,

    #[command(subcommand)]
    command: Command,
}

#[derive(Subcommand)]
enum Command {
    /// Initializes a buffrs setup
    Init {
        /// Sets up the package as lib
        #[clap(long, conflicts_with = "api")]
        #[arg(group = "pkg")]
        lib: bool,
        /// Sets up the package as api
        #[clap(long, conflicts_with = "lib")]
        #[arg(group = "pkg")]
        api: bool,
        /// The package name used for initialization
        #[clap(requires = "pkg")]
        package: Option<PackageName>,
    },

    /// Creates a new buffrs package in the current directory
    New {
        /// Sets up the package as lib
        #[clap(long, conflicts_with = "api")]
        #[arg(group = "pkg")]
        lib: bool,
        /// Sets up the package as api
        #[clap(long, conflicts_with = "lib")]
        #[arg(group = "pkg")]
        api: bool,
        /// The package name
        #[clap(requires = "pkg")]
        package: PackageName,
    },

    /// Check rule violations for this package.
    Lint,

    /// Adds dependencies to a manifest file
    Add {
        /// Artifactory url (e.g. https://<domain>/artifactory)
        #[clap(long)]
        registry: RegistryUri,
        /// Dependency to add (Format <repository>/<package>@<version>
        dependency: String,
    },
    /// Removes dependencies from a manifest file
    #[clap(alias = "rm")]
    Remove {
        /// Package to remove from the dependencies
        package: PackageName,
    },

    /// Exports the current package into a distributable tgz archive
    #[clap(alias = "pack")]
    Package {
        /// Target directory for the released package
        #[clap(long)]
        #[arg(default_value = ".")]
        output_directory: String,
        /// Generate package but do not write it to filesystem
        #[clap(long)]
        dry_run: bool,
        /// Override the version from the manifest
        ///
        /// Note: This overrides the version in the manifest.
        #[clap(long)]
        set_version: Option<Version>,
        /// Indicate whether access time information is preserved when creating a package.
        #[clap(long)]
        #[arg(default_value_t = false)]
        preserve_mtime: bool,
    },

    /// Packages and uploads this api to the registry
    Publish {
        /// Artifactory url (e.g. https://<domain>/artifactory)
        #[clap(long)]
        registry: RegistryUri,
        /// Destination repository for the release
        #[clap(long)]
        repository: String,
        /// Allow a dirty git working tree while publishing
        #[clap(long)]
        allow_dirty: bool,
        /// Abort right before uploading the release to the registry
        #[clap(long)]
        dry_run: bool,
        /// Override the version from the manifest
        ///
        /// Note: This overrides the version in the manifest.
        #[clap(long)]
        set_version: Option<Version>,
        /// Indicate whether access time information is preserved when creating a package.
        #[clap(long)]
        #[arg(default_value_t = true)]
        preserve_mtime: bool,
    },

    /// Installs dependencies
    Install {
        /// Indicate whether access time information is preserved when installing a local.
        #[clap(long)]
        #[arg(default_value_t = true)]
        preserve_local_mtime: bool,
        /// Do not make any network requests.
        ///
        /// All packages must be available in the local cache (or via BUFFRS_CACHE).
        /// Fails with a human-readable error if a package would need to be downloaded.
        #[clap(long)]
        #[arg(default_value_t = false)]
        offline: bool,
    },
    /// Uninstalls dependencies
    Uninstall,

    /// Lists all protobuf files managed by Buffrs to stdout
    #[clap(alias = "ls")]
    List,

    /// Logs you in for a registry
    Login {
        /// Artifactory url (e.g. https://<domain>/artifactory)
        #[clap(long)]
        registry: RegistryUri,
    },
    /// Logs you out from a registry
    Logout {
        /// Artifactory url (e.g. https://<domain>/artifactory)
        #[clap(long)]
        registry: RegistryUri,
    },

    /// Lockfile related commands
    Lock {
        #[command(subcommand)]
        command: LockfileCommand,
    },
}

#[derive(Subcommand)]
enum LockfileCommand {
    /// Prints the file requirements derived from the lockfile serialized as JSON
    ///
    /// This is useful for consumption of the lockfile in other programs.
    PrintFiles,
}

#[tokio::main(flavor = "current_thread")]
async fn main() -> miette::Result<()> {
    human_panic::setup_panic!();

    let cli = Cli::parse();

    tracing_subscriber::fmt()
        .compact()
        .without_time()
        .with_level(cli.verbose)
        .with_file(cli.verbose)
        .with_target(cli.verbose)
        .with_line_number(cli.verbose)
        .with_max_level(if cli.verbose {
            tracing::Level::DEBUG
        } else {
            tracing::Level::INFO
        })
        .fmt_fields(tracing_subscriber::fmt::format::DefaultFields::new())
        .event_format(BuffrsEventFormatter::new(cli.verbose))
        .try_init()
        .unwrap();

    let package = Manifest::name().await.unwrap_or("unknown package".into());

    match cli.command {
        Command::Init { lib, api, package } => {
            let kind = infer_package_type(lib, api);

            command::init(kind, package.to_owned())
                .await
                .wrap_err(miette!(
                    "failed to initialize {}",
                    package.map(|p| format!("`{p}`")).unwrap_or_default()
                ))
        }
        Command::New { lib, api, package } => {
            let kind = infer_package_type(lib, api);

            command::new(kind, package.to_owned())
                .await
                .wrap_err(miette!("failed to initialize {}", format!("`{package}`")))
        }
        Command::Login { registry } => command::login(registry.to_owned())
            .await
            .wrap_err(miette!("failed to login to `{registry}`")),
        Command::Logout { registry } => command::logout(registry.to_owned())
            .await
            .wrap_err(miette!("failed to logout from `{registry}`")),
        Command::Add {
            registry,
            dependency,
        } => command::add(registry.to_owned(), &dependency)
            .await
            .wrap_err(miette!(
                "failed to add `{dependency}` from `{registry}` to `{MANIFEST_FILE}`"
            )),

        Command::Remove { package } => command::remove(package.to_owned()).await.wrap_err(miette!(
            "failed to remove `{package}` from `{MANIFEST_FILE}`"
        )),
        Command::Package {
            output_directory,
            dry_run,
            set_version,
            preserve_mtime,
        } => command::package(output_directory, dry_run, set_version, preserve_mtime)
            .await
            .wrap_err(miette!(
                "failed to export `{package}` into the buffrs package format"
            )),
        Command::Publish {
            registry,
            repository,
            allow_dirty,
            dry_run,
            set_version,
            preserve_mtime,
        } => command::publish(
            registry.to_owned(),
            repository.to_owned(),
            allow_dirty,
            dry_run,
            set_version,
            preserve_mtime,
        )
        .await
        .wrap_err(miette!(
            "failed to publish `{package}` to `{registry}:{repository}`",
        )),
        Command::Lint => command::lint().await.wrap_err(miette!(
            "failed to lint protocol buffers in `{}`",
            PackageStore::PROTO_PATH
        )),
        Command::Install {
            preserve_local_mtime,
            offline,
        } => {
            let network_mode = if offline {
                NetworkMode::Offline
            } else {
                NetworkMode::Online
            };
            command::install(preserve_local_mtime, network_mode)
                .await
                .wrap_err(miette!("failed to install dependencies for `{package}`"))
        }
        Command::Uninstall => command::uninstall()
            .await
            .wrap_err(miette!("failed to uninstall dependencies for `{package}`")),
        Command::List => command::list().await.wrap_err(miette!(
            "failed to list installed protobuf files for `{package}`"
        )),
        Command::Lock { command } => match command {
            LockfileCommand::PrintFiles => command::lock::print_files().await.wrap_err(miette!(
                "failed to print locked file requirements of `{package}`"
            )),
        },
    }
}

fn infer_package_type(lib: bool, api: bool) -> Option<PackageType> {
    if lib {
        Some(PackageType::Lib)
    } else if api {
        Some(PackageType::Api)
    } else {
        None
    }
}