// Copyright (C) 2026 Doan Luu Duc Tai // // 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 . use std::fs; use std::path::Path; use anyhow::{Context, Result, ensure}; use crate::project::{Mode, Project}; use crate::{files, images, process}; pub(crate) fn create(project: &Project, destination: &Path) -> Result<()> { crate::check(project)?; process::run(project.command("cargo")?.args([ "build", "--locked", "--release", "--target", "x86_64-unknown-linux-musl", ]))?; files::write( &project.root.join("bin/stack"), &fs::read( project .root .join("target/x86_64-unknown-linux-musl/release/stack"), )?, 0o755, )?; let mut settings = project.settings()?; settings.mode = Mode::Local; settings.online = true; for service in project.configuration(&settings)?.services.values() { project.ensure_image(&settings, service)?; } files::private_dir(destination)?; let temp = tempfile::Builder::new() .prefix(".release-") .tempdir_in(destination)?; let stage = temp.path().join("stack"); files::private_dir(&stage)?; files::copy_source(&project.root, &stage)?; fs::create_dir_all(stage.join("bin"))?; fs::copy( project .root .join("target/x86_64-unknown-linux-musl/release/stack"), stage.join("bin/stack"), )?; images::save( project, &images::configured(project, &settings)?, &stage.join("var"), )?; files::manifest(&stage, "release")?; let output = destination.join(format!( "stack-release-{}.tar.gz", chrono::Utc::now().format("%Y%m%dT%H%M%SZ") )); files::pack(&stage, &output)?; let filename = output .file_name() .context("Missing archive name")? .to_string_lossy(); files::write( &output.with_extension("gz.sha256"), format!("{} {filename}\n", files::hash_file(&output)?).as_bytes(), 0o600, )?; process::line(format_args!("Release: {}", output.display())) } pub(crate) fn activate(archive: &Path, project: &Project) -> Result<()> { let temp = tempfile::Builder::new() .prefix(".activate-") .tempdir_in(&project.root)?; let stage = temp.path().join("stack"); files::extract(archive, &stage)?; ensure!( files::verify(&stage)?.kind == "release", "Activation requires a release archive" ); let pending = Project::at(&stage)?; let mut settings = project.settings()?; settings.mode = Mode::Production; settings.online = false; let lock = stage.join("var/image-lock.json"); let info: serde_json::Value = serde_json::from_slice(&fs::read(&lock)?)?; ensure!( info.get("platform").and_then(serde_json::Value::as_str) == Some(project.platform()?.as_str()), "Release architecture does not match the server" ); let _config = pending.configuration(&settings)?; for name in files::SOURCE { let source = stage.join(name); if source.exists() { let target = project.root.join(name); if target.is_dir() { files::regular_tree(&target)?; fs::remove_dir_all(&target)?; } files::copy_tree(&source, &target)?; } } files::write( &project.root.join("bin/stack"), &fs::read(stage.join("bin/stack"))?, 0o755, )?; for name in ["images.tar", "image-lock.json"] { files::copy_tree( &stage.join("var").join(name), &project.root.join("var").join(name), )?; } images::prepare(project, &mut settings)?; project.up(&settings) }