aboutsummaryrefslogtreecommitdiff
path: root/src/release.rs
diff options
context:
space:
mode:
authorDoan Luu Duc Tai <doan@luuduct.ai>2026-10-30 14:00:58 -0500
committerDoan Luu Duc Tai <doan@luuduct.ai>2026-10-30 14:00:58 -0500
commit19a0280d7dabd4ca637d8070cc7d3691a0e85bd1 (patch)
treede4591ed54ef860f04dd88bb3aae51dee51237a7 /src/release.rs
parent006a1e94075e90fccfe248cd66bf7d0586a86279 (diff)
Manage the portable stack with Rust and rootless Podman
Diffstat (limited to 'src/release.rs')
-rw-r--r--src/release.rs131
1 files changed, 131 insertions, 0 deletions
diff --git a/src/release.rs b/src/release.rs
new file mode 100644
index 0000000..88b4093
--- /dev/null
+++ b/src/release.rs
@@ -0,0 +1,131 @@
+// 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/>.
+
+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)
+}