diff options
Diffstat (limited to 'src/main.rs')
| -rw-r--r-- | src/main.rs | 252 |
1 files changed, 252 insertions, 0 deletions
diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..ec8784e --- /dev/null +++ b/src/main.rs @@ -0,0 +1,252 @@ +// Copyright (C) 2026 Doan Luu Duc Tai <doan@luuduct.ai> +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +#![doc = "Rust commands for personal web infrastructure."] + +pub(crate) mod archive; +pub(crate) mod files; +pub(crate) mod images; +pub(crate) mod process; +pub(crate) mod project; +pub(crate) mod release; + +use std::path::{Path, PathBuf}; +use std::process::ExitCode; + +use anyhow::Result; +use clap::{Parser, Subcommand}; + +use crate::project::{Mode, Project}; + +#[derive(Debug, Parser)] +#[command( + name = "stack", + version, + about = "Develop, deploy, and back up the personal web stack" +)] +struct Cli { + #[arg( + long, + global = true, + help = "Application root; otherwise discovered from the working directory or binary" + )] + root: Option<PathBuf>, + #[command(subcommand)] + command: Action, +} + +#[derive(Debug, Subcommand)] +enum Action { + #[command(about = "Start the local development services")] + Dev { + #[arg(long, default_value_t = 8080)] + port: u16, + }, + #[command(about = "Start or update services in the selected mode")] + Up { + #[arg(long, conflicts_with = "local")] + production: bool, + #[arg(long)] + local: bool, + #[arg(long)] + port: Option<u16>, + #[arg(long)] + online: bool, + }, + #[command(about = "Show service status")] + Status, + #[command(about = "Stop services while retaining data")] + Down, + #[command(about = "Create a GPG-encrypted application export")] + Backup { + destination: Option<PathBuf>, + #[arg(long)] + include_images: bool, + #[arg(long, default_value = archive::RECIPIENT)] + recipient: String, + #[arg(long)] + key: Option<PathBuf>, + }, + #[command( + about = "Decrypt, validate, and restore into a new directory without starting services" + )] + Restore { + archive: PathBuf, + destination: PathBuf, + }, + #[command(about = "Run formatting, strict lints, tests, documentation, and dependency checks")] + Check, + #[command(about = "Check and build a portable production release locally")] + Release { destination: Option<PathBuf> }, + #[command(about = "Install a prebuilt release without compiling on the server")] + Activate { archive: PathBuf }, + #[command(about = "Generate the third-party license page with GNU license names")] + Licenses, +} + +fn main() -> ExitCode { + let arguments = match Cli::try_parse() { + Ok(arguments) => arguments, + Err(error) => { + let status = if error.use_stderr() { + ExitCode::FAILURE + } else { + ExitCode::SUCCESS + }; + drop(error.print()); + return status; + } + }; + match execute(arguments) { + Ok(()) => ExitCode::SUCCESS, + Err(error) => { + drop(process::error(format_args!("Error: {error:#}"))); + ExitCode::FAILURE + } + } +} + +fn execute(arguments: Cli) -> Result<()> { + if let Action::Restore { + archive, + destination, + } = &arguments.command + { + return archive::restore(&absolute(archive)?, &absolute(destination)?); + } + let project = Project::discover(arguments.root.as_deref())?; + let _operation = project.lock()?; + match arguments.command { + Action::Dev { port } => { + let mut settings = project.settings()?; + settings.mode = Mode::Local; + settings.port = port; + images::prepare(&project, &mut settings)?; + project.up(&settings) + } + Action::Up { + production, + local, + port, + online, + } => { + let mut settings = project.settings()?; + if production { + settings.mode = Mode::Production; + } + if local { + settings.mode = Mode::Local; + } + if let Some(selected_port) = port { + settings.port = selected_port; + } + if online { + settings.online = true; + } + images::prepare(&project, &mut settings)?; + project.up(&settings) + } + Action::Status => project.status(&project.settings()?), + Action::Down => project.down(&project.settings()?), + Action::Backup { + destination, + include_images, + recipient, + key, + } => { + let output = destination.map_or_else( + || Ok(project.root.join("var/exports")), + |path| absolute(&path), + )?; + archive::backup( + &project, + &output, + include_images, + &recipient, + key.as_deref(), + ) + } + Action::Release { destination } => { + let output = destination + .map_or_else(|| Ok(project.root.join("dist")), |path| absolute(&path))?; + release::create(&project, &output) + } + Action::Activate { archive } => release::activate(&absolute(&archive)?, &project), + Action::Check => check(&project), + Action::Licenses => licenses(&project), + Action::Restore { .. } => anyhow::bail!("Restore action was not handled"), + } +} + +fn absolute(path: &Path) -> Result<PathBuf> { + Ok(if path.is_absolute() { + path.to_path_buf() + } else { + std::env::current_dir()?.join(path) + }) +} + +fn licenses(project: &Project) -> Result<()> { + process::run(project.command("cargo")?.args([ + "about", + "generate", + "about.hbs", + "--output-file", + "web/THIRD-PARTY-LICENSES.html", + ])) +} + +fn check(project: &Project) -> Result<()> { + process::run( + project + .command("cargo")? + .args(["fmt", "--all", "--", "--check"]), + )?; + process::run(project.command("cargo")?.args([ + "clippy", + "--locked", + "--workspace", + "--all-targets", + "--all-features", + "--", + "-D", + "warnings", + ]))?; + process::run(project.command("cargo")?.args([ + "test", + "--locked", + "--workspace", + "--all-targets", + "--all-features", + ]))?; + process::run( + project + .command("cargo")? + .env("RUSTDOCFLAGS", "-D warnings") + .args([ + "doc", + "--locked", + "--workspace", + "--no-deps", + "--document-private-items", + ]), + )?; + process::run( + project + .command("cargo")? + .args(["deny", "--all-features", "check"]), + )?; + licenses(project) +} |
