abbaye/main.rs
1//! # Abbaye
2//!
3//! 
4//!
5//! Abbaye is a Static Site Generator (SSG) for your software. As GitHub,
6//! Gitea, Forgejo and consorts offer, Abbaye can be used to generate a
7//! website with your software's presentation, documentation, and distribution, per version.
8//!
9//! Here's an example file structure:
10//!
11//! ```text
12//! .
13//! ├── index.html # the main page of the website, enabling choosing a version, defaults to "latest" (contains a list of available versions and a iframe to the selected version?)
14//! ├── releases.feed # the RSS feed of the releases
15//! ├── latest -> v2.0.0 # symlink to the latest version (biggest version number)
16//! ├── v1.0.0/ # the directory containing the version 1.0.0 of the software
17//! │ ├── index.html # the main page of the version 1.0.0, from the README.md file.
18//! │ │ # Contains a sidebar with links to the documentation and distribution.
19//! │ │ # After the readme content, A changelog is displayed.
20//! │ ├── docs/ # the directory containing the documentation of the version 1.0.0
21//! │ │ ├── index.html # the main page of the documentation of the version 1.0.0
22//! │ │ └── …
23//! │ ├── docs.tar.gz # the tarball containing the documentation of the version 1.0.0
24//! │ └── dist/ # the directory containing the distribution of the version 1.0.0
25//! │ ├── source.tgz # the source code of the version 1.0.0
26//! │ ├── mybin-v1.0.0-x86_64-unknown-linux-gnu
27//! │ └── mybin-v1.0.0-x86_64-unknown-linux-musl
28//! └── v2.0.0/ # the directory containing the version 2.0.0 of the software
29//! ├── index.html # the main page of the version 2.0.0
30//! ├── …
31//! └── …
32//! ```
33//!
34//! ## Why ?
35//!
36//! This piece of software is for people that can't or won't use a full-featured forge such as GitHub, GitLab, ForgeJo & others.
37//! These forges provide "release pages" that allow you to upload and distribute your software, as well as get a changelog.
38//!
39//! Abbaye is made to be a simple, lightweight alternative to these forges, for the release/documentation parts.
40//!
41//! ### Why "Abbaye" ?
42//!
43//! [Abbaye](https://en.wikipedia.org/wiki/Abbaye) is a French word for Abbey. An Abbey is a type of monastery, on the big-ish side, but still a small, quiet place.
44//!
45//! Anyway, that's where you store and display your relics (your software releases).
46//!
47//! ## Installation
48//!
49//! ### Pre-built binaries
50//!
51//! You can grab a pre-built binary from the [releases page](http://vit.am/~ololduck/abbaye/latest).
52//!
53//! The `-musl` binaries are statically linked and should run everywhere, while the `-gnu` binaries are dynamically linked and require a compatible system library(which is probably available if you're not using an exotic distribution).
54//!
55//! ### From source
56//!
57//! To build from source, you need to have Rust installed. You can install Rust using [rustup](https://rustup.rs/).
58//! If you don't have rust installed, this project won't be of much use to you, as it currently only implements rust builders :stuck_out_tongue:
59//!
60//! You can clone the [repository](https://git.sr.ht/~ololduck/abbaye) and build the project using `cargo build --release`.
61//!
62//! ## Usage
63//!
64//! Run `abbaye init` in your project's directory to create a `abbaye.toml` configuration file. You can then customize the configuration to your liking.
65//! Here's an example configuration file to get you started:
66//!
67//! ```toml
68//! [site]
69//! name = "Abbaye"
70//! base_url = "http://vit.am/~ololduck/abbaye/" # required for Atom feed generation (canonical URLs are used for feed items)
71//!
72//! [version_extractor]
73//! type = "git" # extract version from git tags
74//! tag_prefix = "v"
75//!
76//! [changelog] # use the default changelog parser (Keepachangelog format in CHANGELOG.md)
77//!
78//! [[builders]] # builds the project using cargo build --release
79//! type = "cargo"
80//! targets = ["x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl"]
81//!
82//! [[builders]] # generates documentation using cargo doc
83//! type = "cargo_doc"
84//! no_deps = true # Don't include dependencies in the documentation
85//!
86//! [[builders]]
87//! type = "archive" # creates a compressed tarball of the source code (can be of anything, really)
88//! ```
89//!
90//! Then run `abbaye build` to build the site. The site will be generated in the `public/` directory by default.
91//! Now you can copy the contents of `public/` to your web server to deploy the site. For instance, with rsync:
92//! `rsync --progress -avz --links --perms --update public/ ololduck@vit.am:public_html/abbaye/`
93//!
94//! ### ✨ Customization ✨
95//!
96//! You can dump the default theme/templates to your local filesystem with `abbaye dump-theme`.
97//!
98//! This will create a `.abbaye/theme/` directory in your current directory with the default templates, which you can then ✨customize✨.
99//!
100//! ## Future plans
101//!
102//! - [x] Add support for theming
103//! - [ ] Add support for more site variables, such as the site title, description, and author, or even a custom footer and stuff.
104//! - [ ] Add support for a `self-update`-like command to update the abbaye binary to the latest version. The mechanisms put in place for this goal should be usable to any user of `abbaye`.
105//!
106//! ## Contributing
107//!
108//! Contributions are welcome! As i am mainly a rust developer, i am open to any contributions that improve the project, especially to support more artifacts builders/types.
109//!
110//! Just clone the repository and {send me an email,contact me on {IRC (ololduck@irc.libera.chat),the Fediverse (@ololduck@fosstodon.org)}} with {a link to your fork,a git patch,compliments and adoration}.
111
112use std::path::PathBuf;
113
114use clap::Parser;
115use human_panic::setup_panic;
116use miette::{IntoDiagnostic, Result};
117use tokio::fs::create_dir_all;
118use tracing::{info, warn};
119
120use crate::{
121 builders::{AnyBuilder, archive::ArchiveBuilderConfig},
122 changelog::ChangelogConfig,
123 config::{AbbayeConfig, SiteConfig},
124 version_extractors::{AnyVersionExtractor, git::GitVersionConfig},
125};
126
127/// All builders for the site (ex: cargo build, cargo doc, etc.).
128pub mod builders;
129/// Parses the changelog file and generates a changelog page for the site.
130pub mod changelog;
131mod cli;
132/// Handles the `abbaye.toml` configuration file.
133pub mod config;
134/// Generates the site from the configuration and builds it.
135pub mod site;
136/// Extracts current version information from different sources (ex: git tags, cargo metadata, etc.).
137pub mod version_extractors;
138
139/// Build the full website for every git tag, sorted from the lowest semver
140/// version to the highest.
141///
142/// For each tag the function:
143/// 1. Runs `git checkout <tag>` to switch the working tree.
144/// 2. Loads `abbaye.toml` from the checked-out revision (falling back to the
145/// config that was active before the loop if the file is absent).
146/// 3. Calls [`site::build_site`] to produce the version page and update the
147/// root index and Atom feed.
148///
149/// The original HEAD (branch or commit) is always restored after the loop,
150/// even when an error occurs.
151async fn build_all() -> Result<()> {
152 // Load the current config to discover the version extractor settings.
153 let base_config = config::load_config()?;
154
155 // `git for-each-ref --sort=version:refname` returns tags in semver order,
156 // lowest first, which is exactly the order we want.
157 let all_versions = base_config.version_extractor.extract_all().await?;
158 if all_versions.is_empty() {
159 info!("No tagged versions found – nothing to build.");
160 return Ok(());
161 }
162
163 // Remember where we are so we can restore it when we're done.
164 // Prefer the branch name (symbolic ref) so that checking it out
165 // afterwards leaves the user on their branch rather than in a
166 // detached-HEAD state. Fall back to the raw commit SHA when HEAD
167 // is already detached.
168 let symref_out = tokio::process::Command::new("git")
169 .args(["symbolic-ref", "--short", "HEAD"])
170 .output()
171 .await
172 .into_diagnostic()?;
173 let original_head = if symref_out.status.success() {
174 // On a branch.
175 String::from_utf8(symref_out.stdout)
176 .into_diagnostic()?
177 .trim()
178 .to_owned()
179 } else {
180 // Detached HEAD — fall back to the commit SHA.
181 let sha_out = tokio::process::Command::new("git")
182 .args(["rev-parse", "HEAD"])
183 .output()
184 .await
185 .into_diagnostic()?;
186 if !sha_out.status.success() {
187 return Err(miette::miette!("Could not determine current HEAD"));
188 }
189 String::from_utf8(sha_out.stdout)
190 .into_diagnostic()?
191 .trim()
192 .to_owned()
193 };
194
195 let total = all_versions.len();
196 info!("Building {} version(s) …", total);
197
198 // Run the build loop; capture the result so we can restore HEAD first.
199 let loop_result = async {
200 for (i, version_info) in all_versions.iter().enumerate() {
201 let tag = base_config
202 .version_extractor
203 .tag_name(&version_info.version);
204
205 info!("[{}/{}] Checking out {} …", i + 1, total, tag);
206
207 let checkout = tokio::process::Command::new("git")
208 .args(["checkout", &tag])
209 .output()
210 .await
211 .into_diagnostic()?;
212 if !checkout.status.success() {
213 let stderr = String::from_utf8_lossy(&checkout.stderr);
214 return Err(miette::miette!("git checkout {tag} failed:\n{stderr}"));
215 }
216
217 // Reload `abbaye.toml` from the checked-out revision so the build
218 // uses that version's own configuration (builders, readme path,
219 // etc.). If the file does not exist in this revision, fall back
220 // to the config we loaded before the loop.
221 let version_config = config::load_config().unwrap_or_else(|_| base_config.clone());
222
223 info!(
224 "[{}/{}] Building version {} …",
225 i + 1,
226 total,
227 version_info.version
228 );
229
230 site::build_site(version_config).await?;
231 }
232 Ok(())
233 }
234 .await;
235
236 // Always restore HEAD, regardless of whether the loop succeeded.
237 let restore = tokio::process::Command::new("git")
238 .args(["checkout", &original_head])
239 .output()
240 .await
241 .into_diagnostic()?;
242 if !restore.status.success() {
243 let stderr = String::from_utf8_lossy(&restore.stderr);
244 warn!("Could not restore HEAD to {original_head}:\n{stderr}");
245 }
246
247 loop_result?;
248 info!("Done. Built {total} version(s).");
249 Ok(())
250}
251
252#[tokio::main]
253async fn main() -> Result<()> {
254 setup_panic!();
255 let cli_args = cli::CliArgs::parse();
256
257 tracing_subscriber::fmt()
258 .with_timer(tracing_subscriber::fmt::time::SystemTime)
259 .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
260 .init();
261
262 match cli_args.command {
263 cli::CliCommand::Init { path } => {
264 let base_path = if let Some(path) = path {
265 path
266 } else {
267 std::env::current_dir().into_diagnostic()?
268 };
269 if base_path.join("abbaye.toml").exists() {
270 return Err(miette::miette!(
271 "abbaye.toml already exists in this directory"
272 ));
273 }
274 let abbaye_config = AbbayeConfig {
275 site: SiteConfig {
276 name: "MyProject Release Page".to_string(),
277 readme: None,
278 base_url: None,
279 repo_url: None,
280 lang: None,
281 },
282 version_extractor: AnyVersionExtractor::Git(GitVersionConfig {
283 tag_prefix: Some("v".to_string()),
284 dirty_suffix: "-dirty".to_string(),
285 }),
286 builders: vec![AnyBuilder::Archive(ArchiveBuilderConfig {
287 source_dir: None,
288 output: None,
289 prefix: None,
290 ignore_patterns: vec![".git/".to_string(), "*.tar.gz".to_string()],
291 })],
292 changelog: ChangelogConfig {
293 ..Default::default()
294 },
295 output_dir: None,
296 };
297 let config_path = base_path.join("abbaye.toml");
298 let toml = toml::to_string_pretty(&abbaye_config).into_diagnostic()?;
299 tokio::fs::write(&config_path, toml)
300 .await
301 .into_diagnostic()?;
302 }
303 cli::CliCommand::Build => {
304 let config = config::load_config()?;
305 site::build_site(config).await?;
306 }
307 cli::CliCommand::BuildAll => {
308 build_all().await?;
309 }
310 cli::CliCommand::DumpTheme => {
311 let theme_path = PathBuf::from(".abbaye").join("theme");
312 create_dir_all(&theme_path).await.into_diagnostic()?;
313 tokio::fs::write(
314 theme_path.join("root_index.html.j2"),
315 site::TEMPLATE_ROOT_INDEX,
316 )
317 .await
318 .into_diagnostic()?;
319 tokio::fs::write(
320 theme_path.join("version_index.html.j2"),
321 site::TEMPLATE_VERSION_INDEX,
322 )
323 .await
324 .into_diagnostic()?;
325 }
326 }
327 Ok(())
328}