From e7872f56f22708d8c901f1c733b4ec1d23e978a3 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Thu, 30 Apr 2020 21:21:20 -0700 Subject: [PATCH] Move all output from `bin/gen` to `target/gen` To make it clearer what is and isn't generated content, make gen place all generated output in `target/gen`. Also, try to make the readme clearer about the location of build artifacts. type: development --- .github/workflows/build.yaml | 8 +- .gitignore | 12 -- Cargo.lock | 10 +- README.md | 57 +++--- bin/gen/Cargo.toml | 4 +- bin/gen/src/arguments.rs | 16 ++ bin/gen/src/bin.rs | 4 +- bin/gen/src/bin_subcommand.rs | 150 ++++++++++++++ bin/gen/src/common.rs | 13 +- bin/gen/src/error.rs | 22 +- bin/gen/src/main.rs | 6 +- bin/gen/src/opt.rs | 283 -------------------------- bin/gen/src/options.rs | 11 + bin/gen/src/project.rs | 21 +- bin/gen/src/reference_section.rs | 2 +- bin/gen/src/subcommand.rs | 336 +++++++++++++++++++------------ bin/gen/templates/README.md | 52 ++--- bin/package | 31 +-- book/src/SUMMARY.md | 1 + book/src/bittorrent.md | 1 + book/src/changelog.md | 1 + book/src/commands | 1 + book/src/commands.md | 1 + book/src/faq.md | 1 + book/src/introduction.md | 1 + book/src/references | 1 + book/src/references.md | 1 + justfile | 3 +- 28 files changed, 503 insertions(+), 547 deletions(-) create mode 100644 bin/gen/src/arguments.rs create mode 100644 bin/gen/src/bin_subcommand.rs delete mode 100644 bin/gen/src/opt.rs create mode 100644 bin/gen/src/options.rs create mode 120000 book/src/SUMMARY.md create mode 120000 book/src/bittorrent.md create mode 120000 book/src/changelog.md create mode 120000 book/src/commands create mode 120000 book/src/commands.md create mode 120000 book/src/faq.md create mode 120000 book/src/introduction.md create mode 120000 book/src/references create mode 120000 book/src/references.md diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 3212a9d..badbc47 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -108,7 +108,7 @@ jobs: if: matrix.os == 'macos-latest' run: | brew install help2man - cargo run --package gen all + cargo run --package gen -- --bin target/debug/imdl all git diff --no-ext-diff --exit-code - name: Install `mdbook` @@ -118,7 +118,7 @@ jobs: - name: Build Book run: | - cargo run --package gen book + cargo run --package gen -- --bin target/debug/imdl book mdbook build book --dest-dir ../www/book - name: Record Git Revision @@ -139,12 +139,12 @@ jobs: shell: bash run: ./bin/package ${{github.ref}} ${{matrix.os}} ${{matrix.target}} - - name: Publish + - name: Publish Release Archive uses: softprops/action-gh-release@v1 if: startsWith(github.ref, 'refs/tags/') with: draft: false files: ${{steps.package.outputs.archive}} - prerelease: false + prerelease: true env: GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} diff --git a/.gitignore b/.gitignore index 4f0b8af..82273f6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,17 +1,5 @@ **/*.rs.bk -/CHANGELOG.md /book/book -/book/src/SUMMARY.md -/book/src/bittorrent.md -/book/src/changelog.md -/book/src/commands.md -/book/src/commands/ -/book/src/faq.md -/book/src/introduction.md -/book/src/references -/book/src/references.md -/completions -/man /target /wiki /www/book diff --git a/Cargo.lock b/Cargo.lock index d481ad6..551bbac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -354,6 +354,12 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3" +[[package]] +name = "fs_extra" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2a4a2034423744d2cc7ca2068453168dcdb82c438419e639a26bd87839c674" + [[package]] name = "gen" version = "0.0.0" @@ -363,10 +369,9 @@ dependencies = [ "cargo_toml", "chrono", "fehler", + "fs_extra", "git2", "globset", - "ignore", - "lexiclean", "libc", "log", "pretty_env_logger", @@ -379,7 +384,6 @@ dependencies = [ "strum_macros", "tempfile", "url", - "walkdir", ] [[package]] diff --git a/README.md b/README.md index 6623cd2..478236f 100644 --- a/README.md +++ b/README.md @@ -31,10 +31,7 @@ For more about the project and its goals, check out - [Examples](#examples) - [FAQ](#faq) - [Notes for Packagers](#notes-for-packagers) - - [Package Artifacts](#package-artifacts) - - [Binary](#binary) - - [Man Pages](#man-pages) - - [Completion Scripts](#completion-scripts) + - [Build Artifacts](#build-artifacts) - [Release Updates](#release-updates) - [Chat](#chat) - [Contributing](#contributing) @@ -181,46 +178,38 @@ Intermodal is distributed under the a public domain dedication with a fallback all-permissive license. The SPDX identifier of the CC0 is [CC0-1.0](https://spdx.org/licenses/CC0-1.0.html). -### Package Artifacts +### Build Artifacts -There are three primary build artifacts: the binary, the man pages, and the -shell completion scripts. +There are a number of build artifacts: the binary, the man pages, the +changelog, and the shell completion scripts. -#### Binary +The binary is built with `cargo`, and the other artifacts are built `gen`, +located in `bin/gen`. -The binary is called `imdl`, and can be built with: +The binary can be built with: -``` -cargo build --release -``` + cargo build --release -After building, the binary will be present at `target/release/imdl`. +_`gen` requires [`help2man`](https://www.gnu.org/software/help2man/) to be +installed, which is used to generate man pages from subcommand `--help` +strings._ -#### Man Pages +The rest of the build artifacts can be built with `gen`: -Intermodal has a number of subcommands, each of which has a man page. The man -pages are generated from the `--help` text using -[`help2man`](https://www.gnu.org/software/help2man/). + cargo run --package gen -- --bin target/release/imdl all -To generate the man pages, ensure `help2man` is available, and run: +_The path to the built `imdl` executable should be passed to `gen` with the `--bin` flag._ -``` -mkdir -p man -cargo run --package gen man -``` +After running the above commands, the following table shows the location of the +built artifacts. -After building, the man pages will be available in `man`. - -#### Completion Scripts - -Completion scripts are available for a number of shells. To generate them, run: - -``` -mkdir -p completions -cargo run --release completions --dir completions -``` - -After running, the completion scripts will be available in `completions`. +| Artifact | Location | +|--------------------|----------------------------| +| Binary | `target/release/imdl` | +| Man Pages | `target/gen/man/*` | +| Completion Scripts | `target/gen/completions/*` | +| Changelog | `target/gen/CHANGELOG.md` | +| Readme | `target/gen/README.md` | ### Release Updates diff --git a/bin/gen/Cargo.toml b/bin/gen/Cargo.toml index 3d88639..1279cfa 100644 --- a/bin/gen/Cargo.toml +++ b/bin/gen/Cargo.toml @@ -11,10 +11,9 @@ askama = "0.9.0" cargo_toml = "0.8.0" chrono = "0.4.11" fehler = "1.0.0" +fs_extra = "1.1.0" git2 = "0.13.1" globset = "0.4.5" -ignore = "0.4.14" -lexiclean = "0.0.1" libc = "0.2.69" log = "0.4.8" pretty_env_logger = "0.4.0" @@ -25,7 +24,6 @@ structopt = "0.3.12" strum = "0.18.0" strum_macros = "0.18.0" tempfile = "3.1.0" -walkdir = "2.3.1" [dependencies.serde] version = "1.0.106" diff --git a/bin/gen/src/arguments.rs b/bin/gen/src/arguments.rs new file mode 100644 index 0000000..d325a94 --- /dev/null +++ b/bin/gen/src/arguments.rs @@ -0,0 +1,16 @@ +use crate::common::*; + +#[derive(StructOpt)] +pub(crate) struct Arguments { + #[structopt(flatten)] + options: Options, + #[structopt(subcommand)] + subcommand: Subcommand, +} + +impl Arguments { + #[throws] + pub(crate) fn run(self) { + self.subcommand.run(self.options)?; + } +} diff --git a/bin/gen/src/bin.rs b/bin/gen/src/bin.rs index f57234f..abd9e69 100644 --- a/bin/gen/src/bin.rs +++ b/bin/gen/src/bin.rs @@ -2,7 +2,7 @@ use crate::common::*; pub(crate) struct Bin { path: PathBuf, - pub(crate) subcommands: Vec, + pub(crate) subcommands: Vec, } impl Bin { @@ -22,7 +22,7 @@ impl Bin { #[throws] fn add_subcommands(&mut self, command: &mut Vec) { - let subcommand = Subcommand::new(&self.path, command.clone())?; + let subcommand = BinSubcommand::new(&self.path, command.clone())?; for name in &subcommand.subcommands { command.push(name.into()); diff --git a/bin/gen/src/bin_subcommand.rs b/bin/gen/src/bin_subcommand.rs new file mode 100644 index 0000000..b84c667 --- /dev/null +++ b/bin/gen/src/bin_subcommand.rs @@ -0,0 +1,150 @@ +use crate::common::*; + +#[derive(Debug, Ord, PartialOrd, Eq, PartialEq)] +pub(crate) struct BinSubcommand { + pub(crate) bin: PathBuf, + pub(crate) command: Vec, + pub(crate) subcommands: Vec, +} + +impl BinSubcommand { + #[throws] + pub(crate) fn new(bin: &Path, command: Vec) -> Self { + let wide_help = Command::new(bin) + .args(command.as_slice()) + .env("IMDL_TERM_WIDTH", "200") + .arg("--help") + .out()?; + + const MARKER: &str = "\nSUBCOMMANDS:\n"; + + let mut subcommands = Vec::new(); + + if let Some(marker) = wide_help.find(MARKER) { + let block = &wide_help[marker + MARKER.len()..]; + + for line in block.lines() { + let name = line.trim().split_whitespace().next().unwrap(); + subcommands.push(name.into()); + } + } + + Self { + bin: bin.into(), + command, + subcommands, + } + } + + #[throws] + fn help(&self) -> String { + info!("Getting help for `{}`", self.command_line()); + + Command::new(&self.bin) + .args(self.command.as_slice()) + .env("IMDL_TERM_WIDTH", "80") + .arg("--help") + .out()? + } + + #[throws] + pub(crate) fn man(&self) -> String { + let command_line = self.command_line(); + + info!("Generating man page for `{}`", command_line); + + let name = command_line.replace(" ", "\\ "); + + let help = self.help()?; + + let description = if self.command.is_empty() { + "A 40' shipping container for the Internet".to_string() + } else { + help.lines().nth(1).unwrap().into() + }; + + let include = format!( + "\ +[NAME] +\\fB{}\\fR +- {} +", + name, description + ); + + let tmp = tempfile::tempdir().context(error::Tempdir)?; + + let include_path = tmp.path().join("include"); + + fs::write(&include_path, include).context(error::Filesystem { + path: &include_path, + })?; + + let version = cmd!(&self.bin, "--version") + .out()? + .split_whitespace() + .nth(1) + .unwrap() + .to_owned(); + + info!("Running help2man for `{}`", command_line); + + let mut command = self.bin.as_os_str().to_owned(); + for arg in &self.command { + command.push(" "); + command.push(arg); + } + + let output = cmd!( + "help2man", + "--include", + &include_path, + "--manual", + "Intermodal Manual", + "--no-info", + "--source", + &format!("Intermodal {}", version), + command + ) + .out()?; + + let man = output + .replace("📦 ", "\n") + .replace("\n.SS ", "\n.SH ") + .replace("\"USAGE:\"", "\"SYNOPSIS:\""); + + let re = Regex::new(r"(?ms).SH DESCRIPTION.*?.SH").unwrap(); + + let man = re.replace(&man, ".SH").into_owned(); + + man + } + + pub(crate) fn slug(&self) -> String { + let mut slug = "imdl".to_string(); + + for name in &self.command { + slug.push('-'); + slug.push_str(&name); + } + + slug + } + + pub(crate) fn command_line(&self) -> String { + let mut line = "imdl".to_string(); + + for name in &self.command { + line.push(' '); + line.push_str(&name); + } + + line + } + + #[throws] + pub(crate) fn page(&self) -> String { + let help = self.help()?; + format!("# `{}`\n```\n{}\n```", self.command_line(), help) + } +} diff --git a/bin/gen/src/common.rs b/bin/gen/src/common.rs index bf2c56d..7a7e65e 100644 --- a/bin/gen/src/common.rs +++ b/bin/gen/src/common.rs @@ -17,8 +17,6 @@ pub(crate) use cargo_toml::Manifest; pub(crate) use chrono::{DateTime, NaiveDateTime, Utc}; pub(crate) use fehler::{throw, throws}; pub(crate) use git2::{Commit, Oid, Repository}; -pub(crate) use ignore::overrides::OverrideBuilder; -pub(crate) use lexiclean::Lexiclean; pub(crate) use libc::EXIT_FAILURE; pub(crate) use log::info; pub(crate) use regex::Regex; @@ -29,7 +27,6 @@ pub(crate) use structopt::StructOpt; pub(crate) use strum::VariantNames; pub(crate) use strum_macros::{EnumVariantNames, IntoStaticStr}; pub(crate) use url::Url; -pub(crate) use walkdir::WalkDir; // modules pub(crate) use crate::error; @@ -39,9 +36,9 @@ pub(crate) use crate::{command_ext::CommandExt, row::Row, slug::Slug, template_e // structs and enums pub(crate) use crate::{ - bin::Bin, changelog::Changelog, config::Config, entry::Entry, error::Error, example::Example, - faq::Faq, faq_entry::FaqEntry, introduction::Introduction, kind::Kind, metadata::Metadata, - opt::Opt, package::Package, project::Project, readme::Readme, reference::Reference, - reference_section::ReferenceSection, release::Release, subcommand::Subcommand, summary::Summary, - table::Table, + arguments::Arguments, bin::Bin, bin_subcommand::BinSubcommand, changelog::Changelog, + config::Config, entry::Entry, error::Error, example::Example, faq::Faq, faq_entry::FaqEntry, + introduction::Introduction, kind::Kind, metadata::Metadata, options::Options, package::Package, + project::Project, readme::Readme, reference::Reference, reference_section::ReferenceSection, + release::Release, subcommand::Subcommand, summary::Summary, table::Table, }; diff --git a/bin/gen/src/error.rs b/bin/gen/src/error.rs index 6937def..1a676b9 100644 --- a/bin/gen/src/error.rs +++ b/bin/gen/src/error.rs @@ -58,6 +58,12 @@ pub(crate) enum Error { dst: PathBuf, source: io::Error, }, + #[snafu(display("I/O error copying `{}` to `{}`: {}", src.display(), dst.display(), source))] + FilesystemRecursiveCopy { + src: PathBuf, + dst: PathBuf, + source: fs_extra::error::Error, + }, #[snafu(display("Git error: {}", source))] Git { source: git2::Error }, #[snafu(display("Regex compilation error: {}", source))] @@ -73,10 +79,6 @@ pub(crate) enum Error { TemplateRender { source: askama::Error }, #[snafu(display("Failed to get workdir for repo at `{}`", repo.display()))] Workdir { repo: PathBuf }, - #[snafu(display("Failed to build overrides: {}", source))] - Ignore { source: ignore::Error }, - #[snafu(display("Failed to traverse worktree: {}", source))] - Walkdir { source: walkdir::Error }, #[snafu(display("Failed to strip path prefix: {}", source))] StripPrefix { source: StripPrefixError }, } @@ -98,15 +100,3 @@ impl From for Error { Self::CargoToml { source } } } - -impl From for Error { - fn from(source: ignore::Error) -> Self { - Self::Ignore { source } - } -} - -impl From for Error { - fn from(source: walkdir::Error) -> Self { - Self::Walkdir { source } - } -} diff --git a/bin/gen/src/main.rs b/bin/gen/src/main.rs index 75b972f..4de4dce 100644 --- a/bin/gen/src/main.rs +++ b/bin/gen/src/main.rs @@ -3,7 +3,9 @@ use crate::common::*; #[macro_use] mod cmd; +mod arguments; mod bin; +mod bin_subcommand; mod changelog; mod command_ext; mod common; @@ -16,7 +18,7 @@ mod faq_entry; mod introduction; mod kind; mod metadata; -mod opt; +mod options; mod package; mod project; mod readme; @@ -33,7 +35,7 @@ mod template_ext; fn main() { pretty_env_logger::init(); - if let Err(error) = Opt::from_args().run() { + if let Err(error) = Arguments::from_args().run() { let bold = Style::new().bold(); let red = Style::new().fg(ansi_term::Color::Red).bold(); eprintln!("{}: {}", red.paint("error"), bold.paint(error.to_string())); diff --git a/bin/gen/src/opt.rs b/bin/gen/src/opt.rs deleted file mode 100644 index 9b72e24..0000000 --- a/bin/gen/src/opt.rs +++ /dev/null @@ -1,283 +0,0 @@ -use crate::common::*; - -#[derive(StructOpt)] -pub(crate) enum Opt { - #[structopt(about("Update all generated docs"))] - All, - #[structopt(about("Generate book"))] - Book, - #[structopt(about("Generate the changelog"))] - Changelog, - #[structopt(about("Print a commit template to standard output"))] - CommitTemplate, - #[structopt(about("Print possible values for `type` field of commit metadata"))] - CommitTypes, - #[structopt(about("Generate completion scripts"))] - CompletionScripts, - #[structopt(about("Diff generated content between commits"))] - Diff, - #[structopt(about("Generate readme"))] - Readme, - #[structopt(about("Generate man pages"))] - Man, -} - -#[throws] -fn blank(path: impl AsRef, title: &str) { - let path = path.as_ref(); - - info!("Writing blank page to `{}`…", path.display()); - - let text = format!( - " - # {} - - This page intentionally left blank. - ", - title - ); - - fs::write(&path, text).context(error::Filesystem { path })?; -} - -#[throws] -fn clean_dir(path: impl AsRef) { - let path = path.as_ref(); - - info!("Cleaning `{}`…", path.display()); - - if path.is_dir() { - fs::remove_dir_all(path).context(error::Filesystem { path: &path })?; - } - - fs::create_dir_all(path).context(error::Filesystem { path: &path })?; -} - -impl Opt { - #[throws] - pub(crate) fn run(self) { - let project = Project::load()?; - - match self { - Self::Changelog => Self::changelog(&project)?, - Self::CommitTemplate => { - println!("{}", Metadata::default().to_string()); - } - Self::CommitTypes => { - for kind in Kind::VARIANTS { - println!("{}", kind) - } - } - Self::CompletionScripts => Self::completion_scripts(&project)?, - Self::Readme => Self::readme(&project)?, - Self::Book => Self::book(&project)?, - Self::Man => Self::man(&project)?, - Self::Diff => Self::diff(&project)?, - Self::All => Self::all(&project)?, - } - } - - #[throws] - pub(crate) fn all(project: &Project) { - Self::changelog(&project)?; - Self::completion_scripts(&project)?; - Self::readme(&project)?; - Self::book(&project)?; - Self::man(&project)?; - } - - #[throws] - pub(crate) fn changelog(project: &Project) { - info!("Generating changelog…"); - let changelog = Changelog::new(&project)?; - - let path = project.root.join("CHANGELOG.md"); - - fs::write(&path, changelog.render(false)?).context(error::Filesystem { path })?; - } - - #[throws] - pub(crate) fn completion_scripts(project: &Project) { - info!("Generating completion scripts…"); - let completions = project.root.join("completions"); - - clean_dir(&completions)?; - - cmd!( - "cargo", - "run", - "--package", - "imdl", - "completions", - "--dir", - completions - ) - .status_into_result()? - } - - #[throws] - pub(crate) fn diff(project: &Project) { - let tmp = tempfile::tempdir().context(error::Tempdir)?; - - let generated = &[ - "/CHANGELOG.md", - "/README.md", - "/book/src/SUMMARY.md", - "/book/src/bittorrent.md", - "/book/src/changelog.md", - "/book/src/commands.md", - "/book/src/commands/*", - "/book/src/faq.md", - "/book/src/introduction.md", - "/book/src/references.md", - "/book/src/references/*", - "/completions/*", - "/man/*", - ]; - - let gen = |name: &str| -> Result<(), Error> { - cmd!("cargo", "run", "--package", "gen", "all").status_into_result()?; - - let dir = tmp.path().join(name); - - fs::create_dir(&dir).context(error::Filesystem { path: &dir })?; - - let mut builder = OverrideBuilder::new(&project.root); - - for pattern in generated { - builder.add(pattern)?; - } - - let overrides = builder.build()?; - - for result in WalkDir::new(&project.root) { - let entry = result?; - - let src = entry.path(); - - if src.is_dir() { - continue; - } - - if !overrides.matched(&src, false).is_whitelist() { - continue; - } - - let relative = src - .strip_prefix(&project.root) - .context(error::StripPrefix)?; - - let dst = dir.join(relative); - - let dst_dir = dst.join("..").lexiclean(); - - fs::create_dir_all(&dst_dir).context(error::Filesystem { path: dst_dir })?; - - fs::copy(&src, &dst).context(error::FilesystemCopy { src, dst })?; - } - - Ok(()) - }; - - const HEAD: &str = "HEAD"; - - gen(HEAD)?; - - let head = project.repo.head()?; - - let head_commit = head.peel_to_commit()?; - - let parent = head_commit.parent(0)?; - - let parent_hash = parent.id().to_string(); - - cmd!("git", "checkout", &parent_hash).status_into_result()?; - - gen(&parent_hash)?; - - cmd!("colordiff", "-ur", parent_hash, HEAD) - .current_dir(tmp.path()) - .status_into_result() - .ok(); - - cmd!( - "git", - "checkout", - head - .shorthand() - .map(str::to_owned) - .unwrap_or_else(|| head_commit.id().to_string()) - ) - .status_into_result()?; - } - - #[throws] - pub(crate) fn readme(project: &Project) { - info!("Generating readme…"); - - let template = project.root.join("bin/gen/templates/README.md"); - - let readme = Readme::load(&project.config, &template)?; - - let text = readme.render_newline()?; - - let path = project.root.join("README.md"); - fs::write(&path, text).context(error::Filesystem { path })?; - } - - #[throws] - pub(crate) fn book(project: &Project) { - info!("Generating book…"); - - blank(project.root.join("book/src/commands.md"), "Commands")?; - blank(project.root.join("book/src/bittorrent.md"), "BitTorrent")?; - blank(project.root.join("book/src/references.md"), "References")?; - - let commands = project.root.join("book/src/commands/"); - - clean_dir(&commands)?; - - for subcommand in &project.bin.subcommands { - let page = subcommand.page()?; - - let dst = commands.join(format!("{}.md", subcommand.slug())); - - fs::write(&dst, page).context(error::Filesystem { path: dst })?; - } - - clean_dir(&project.root.join("book/src/references/"))?; - - for section in &project.config.references { - section.render_to(project.root.join("book/src").join(section.path()))?; - } - - Faq::new(&project.config.faq).render_to(project.root.join("book/src/faq.md"))?; - - Summary::new(project).render_to(project.root.join("book/src/SUMMARY.md"))?; - - Introduction::new(&project.config).render_to(project.root.join("book/src/introduction.md"))?; - - let changelog = Changelog::new(&project)?; - - let dst = project.root.join("book/src/changelog.md"); - fs::write(&dst, changelog.render(true)?).context(error::Filesystem { path: dst })?; - } - - #[throws] - pub(crate) fn man(project: &Project) { - info!("Generating man pages…"); - let mans = project.root.join("man"); - - clean_dir(&mans)?; - - for subcommand in &project.bin.subcommands { - let man = subcommand.man()?; - - let dst = mans.join(format!("{}.1", subcommand.slug())); - - info!("Writing man page to `{}`", dst.display()); - - fs::write(&dst, man).context(error::Filesystem { path: dst })?; - } - } -} diff --git a/bin/gen/src/options.rs b/bin/gen/src/options.rs new file mode 100644 index 0000000..b2c7eb1 --- /dev/null +++ b/bin/gen/src/options.rs @@ -0,0 +1,11 @@ +use crate::common::*; + +#[derive(StructOpt)] +pub(crate) struct Options { + #[structopt( + long("bin"), + value_name("EXECUTABLE"), + help("Path to the `imdl` binary.") + )] + pub(crate) bin: PathBuf, +} diff --git a/bin/gen/src/project.rs b/bin/gen/src/project.rs index 4f10f11..1201b15 100644 --- a/bin/gen/src/project.rs +++ b/bin/gen/src/project.rs @@ -5,11 +5,12 @@ pub(crate) struct Project { pub(crate) root: PathBuf, pub(crate) config: Config, pub(crate) bin: Bin, + pub(crate) executable: PathBuf, } impl Project { #[throws] - pub(crate) fn load() -> Self { + pub(crate) fn load(options: &Options) -> Self { let start_dir = env::current_dir().context(error::CurrentDir)?; let repo = Repository::discover(&start_dir).context(error::RepositoryDiscover { start_dir })?; @@ -23,7 +24,7 @@ impl Project { let config = Config::load(&root)?; - let bin = Bin::new(&root.join("target/debug/imdl"))?; + let bin = Bin::new(&options.bin)?; let example_commands = config .examples @@ -45,10 +46,22 @@ impl Project { } Project { + executable: options.bin.clone(), + bin, + config, repo, root, - config, - bin, } } + + #[throws] + pub(crate) fn gen(&self) -> PathBuf { + let gen = self.root.join("target").join("gen"); + + if !gen.is_dir() { + fs::create_dir_all(&gen).context(error::Filesystem { path: &gen })?; + } + + gen + } } diff --git a/bin/gen/src/reference_section.rs b/bin/gen/src/reference_section.rs index e014d6a..239910a 100644 --- a/bin/gen/src/reference_section.rs +++ b/bin/gen/src/reference_section.rs @@ -9,6 +9,6 @@ pub(crate) struct ReferenceSection { impl ReferenceSection { pub(crate) fn path(&self) -> String { - format!("./references/{}.md", self.title.slug()) + format!("references/{}.md", self.title.slug()) } } diff --git a/bin/gen/src/subcommand.rs b/bin/gen/src/subcommand.rs index 66b2b84..29b9daa 100644 --- a/bin/gen/src/subcommand.rs +++ b/bin/gen/src/subcommand.rs @@ -1,164 +1,240 @@ use crate::common::*; -#[derive(Debug, Ord, PartialOrd, Eq, PartialEq)] -pub(crate) struct Subcommand { - pub(crate) bin: PathBuf, - pub(crate) command: Vec, - pub(crate) subcommands: Vec, +#[derive(StructOpt)] +pub(crate) enum Subcommand { + #[structopt(about("Update all generated docs"))] + All, + #[structopt(about("Generate book"))] + Book, + #[structopt(about("Generate the changelog"))] + Changelog, + #[structopt(about("Print a commit template to standard output"))] + CommitTemplate, + #[structopt(about("Print possible values for `type` field of commit metadata"))] + CommitTypes, + #[structopt(about("Generate completion scripts"))] + CompletionScripts, + #[structopt(about("Diff generated content between commits"))] + Diff, + #[structopt(about("Generate readme"))] + Readme, + #[structopt(about("Generate man pages"))] + Man, +} + +#[throws] +fn blank(path: impl AsRef, title: &str) { + let path = path.as_ref(); + + info!("Writing blank page to `{}`…", path.display()); + + let text = format!( + " + # {} + + This page intentionally left blank. + ", + title + ); + + fs::write(&path, text).context(error::Filesystem { path })?; +} + +#[throws] +fn clean_dir(path: impl AsRef) { + let path = path.as_ref(); + + info!("Cleaning `{}`…", path.display()); + + if path.is_dir() { + fs::remove_dir_all(path).context(error::Filesystem { path: &path })?; + } + + fs::create_dir_all(path).context(error::Filesystem { path: &path })?; } impl Subcommand { #[throws] - pub(crate) fn new(bin: &Path, command: Vec) -> Self { - let wide_help = Command::new(bin) - .args(command.as_slice()) - .env("IMDL_TERM_WIDTH", "200") - .arg("--help") - .out()?; + pub(crate) fn run(self, options: Options) { + let project = Project::load(&options)?; - const MARKER: &str = "\nSUBCOMMANDS:\n"; - - let mut subcommands = Vec::new(); - - if let Some(marker) = wide_help.find(MARKER) { - let block = &wide_help[marker + MARKER.len()..]; - - for line in block.lines() { - let name = line.trim().split_whitespace().next().unwrap(); - subcommands.push(name.into()); + match self { + Self::Changelog => Self::changelog(&project)?, + Self::CommitTemplate => { + println!("{}", Metadata::default().to_string()); } - } - - Self { - bin: bin.into(), - command, - subcommands, + Self::CommitTypes => { + for kind in Kind::VARIANTS { + println!("{}", kind) + } + } + Self::CompletionScripts => Self::completion_scripts(&project)?, + Self::Readme => Self::readme(&project)?, + Self::Book => Self::book(&project)?, + Self::Man => Self::man(&project)?, + Self::Diff => Self::diff(&project)?, + Self::All => Self::all(&project)?, } } #[throws] - fn help(&self) -> String { - info!("Getting help for `{}`", self.command_line()); - - Command::new(&self.bin) - .args(self.command.as_slice()) - .env("IMDL_TERM_WIDTH", "80") - .arg("--help") - .out()? + pub(crate) fn all(project: &Project) { + Self::changelog(&project)?; + Self::completion_scripts(&project)?; + Self::readme(&project)?; + Self::book(&project)?; + Self::man(&project)?; } #[throws] - pub(crate) fn man(&self) -> String { - let command_line = self.command_line(); + pub(crate) fn changelog(project: &Project) { + info!("Generating changelog…"); + let changelog = Changelog::new(&project)?; - info!("Generating man page for `{}`", command_line); + let path = project.gen()?.join("CHANGELOG.md"); - let name = command_line.replace(" ", "\\ "); + fs::write(&path, changelog.render(false)?).context(error::Filesystem { path })?; + } - let help = self.help()?; + #[throws] + pub(crate) fn completion_scripts(project: &Project) { + info!("Generating completion scripts…"); + let completions = project.gen()?.join("completions"); - let description = if self.command.is_empty() { - "A 40' shipping container for the Internet".to_string() - } else { - help.lines().nth(1).unwrap().into() - }; + clean_dir(&completions)?; - let include = format!( - "\ -[NAME] -\\fB{}\\fR -- {} -", - name, description - ); + cmd!(&project.executable, "completions", "--dir", completions).status_into_result()? + } + #[throws] + pub(crate) fn diff(project: &Project) { let tmp = tempfile::tempdir().context(error::Tempdir)?; - let include_path = tmp.path().join("include"); + let gen = |name: &str| -> Result<(), Error> { + let src = Path::new("target/gen"); - fs::write(&include_path, include).context(error::Filesystem { - path: &include_path, - })?; + fs::remove_dir_all(src).context(error::Filesystem { path: src })?; - let version = cmd!(&self.bin, "--version") - .out()? - .split_whitespace() - .nth(1) - .unwrap() - .to_owned(); + cmd!("cargo", "run", "--package", "gen", "all").status_into_result()?; - info!("Running help2man for `{}`", command_line); + let dir = tmp.path().join(name); - let mut command = self.bin.as_os_str().to_owned(); - for arg in &self.command { - command.push(" "); - command.push(arg); - } + fs::create_dir(&dir).context(error::Filesystem { path: &dir })?; - let output = cmd!( - "help2man", - "--include", - &include_path, - "--manual", - "Intermodal Manual", - "--no-info", - "--source", - &format!("Intermodal {}", version), - command + fs_extra::dir::copy(src, &dir, &fs_extra::dir::CopyOptions::new()) + .context(error::FilesystemRecursiveCopy { src, dst: dir })?; + + Ok(()) + }; + + const HEAD: &str = "HEAD"; + + gen(HEAD)?; + + let head = project.repo.head()?; + + let head_commit = head.peel_to_commit()?; + + let parent = head_commit.parent(0)?; + + let parent_hash = parent.id().to_string(); + + cmd!("git", "checkout", &parent_hash).status_into_result()?; + + gen(&parent_hash)?; + + cmd!("colordiff", "-ur", parent_hash, HEAD) + .current_dir(tmp.path()) + .status_into_result() + .ok(); + + cmd!( + "git", + "checkout", + head + .shorthand() + .map(str::to_owned) + .unwrap_or_else(|| head_commit.id().to_string()) ) - .out()?; - - let man = output - .replace("📦 ", "\n") - .replace("\n.SS ", "\n.SH ") - .replace("\"USAGE:\"", "\"SYNOPSIS:\""); - - let re = Regex::new(r"(?ms).SH DESCRIPTION.*?.SH").unwrap(); - - let man = re.replace(&man, ".SH").into_owned(); - - man - } - - pub(crate) fn slug(&self) -> String { - let mut slug = self - .bin - .display() - .to_string() - .split('/') - .last() - .unwrap() - .to_owned(); - - for name in &self.command { - slug.push('-'); - slug.push_str(&name); - } - - slug - } - - pub(crate) fn command_line(&self) -> String { - let mut line = self - .bin - .display() - .to_string() - .split('/') - .last() - .unwrap() - .to_owned(); - - for name in &self.command { - line.push(' '); - line.push_str(&name); - } - - line + .status_into_result()?; } #[throws] - pub(crate) fn page(&self) -> String { - let help = self.help()?; - format!("# `{}`\n```\n{}\n```", self.command_line(), help) + pub(crate) fn readme(project: &Project) { + info!("Generating readme…"); + + let template = project.root.join("bin/gen/templates/README.md"); + + let readme = Readme::load(&project.config, &template)?; + + let text = readme.render_newline()?; + + let path = project.gen()?.join("README.md"); + fs::write(&path, &text).context(error::Filesystem { path })?; + + let path = project.root.join("README.md"); + fs::write(&path, &text).context(error::Filesystem { path })?; + } + + #[throws] + pub(crate) fn book(project: &Project) { + info!("Generating book…"); + + let gen = project.gen()?; + + let out = gen.join("book"); + + fs::create_dir_all(&out).context(error::Filesystem { path: &out })?; + + blank(out.join("commands.md"), "Commands")?; + blank(out.join("bittorrent.md"), "BitTorrent")?; + blank(out.join("references.md"), "References")?; + + let commands = out.join("commands"); + + clean_dir(&commands)?; + + for subcommand in &project.bin.subcommands { + let page = subcommand.page()?; + + let dst = commands.join(format!("{}.md", subcommand.slug())); + + fs::write(&dst, page).context(error::Filesystem { path: dst })?; + } + + clean_dir(&out.join("references"))?; + + for section in &project.config.references { + section.render_to(out.join(section.path()))?; + } + + Faq::new(&project.config.faq).render_to(out.join("faq.md"))?; + + Summary::new(project).render_to(out.join("SUMMARY.md"))?; + + Introduction::new(&project.config).render_to(out.join("introduction.md"))?; + + let changelog = Changelog::new(&project)?; + + let dst = out.join("changelog.md"); + fs::write(&dst, changelog.render(true)?).context(error::Filesystem { path: dst })?; + } + + #[throws] + pub(crate) fn man(project: &Project) { + info!("Generating man pages…"); + let mans = project.gen()?.join("man"); + + clean_dir(&mans)?; + + for subcommand in &project.bin.subcommands { + let man = subcommand.man()?; + + let dst = mans.join(format!("{}.1", subcommand.slug())); + + info!("Writing man page to `{}`", dst.display()); + + fs::write(&dst, man).context(error::Filesystem { path: dst })?; + } } } diff --git a/bin/gen/templates/README.md b/bin/gen/templates/README.md index fd3c316..1576cc5 100644 --- a/bin/gen/templates/README.md +++ b/bin/gen/templates/README.md @@ -154,46 +154,38 @@ Intermodal is distributed under the a public domain dedication with a fallback all-permissive license. The SPDX identifier of the CC0 is [CC0-1.0](https://spdx.org/licenses/CC0-1.0.html). -### Package Artifacts +### Build Artifacts -There are three primary build artifacts: the binary, the man pages, and the -shell completion scripts. +There are a number of build artifacts: the binary, the man pages, the +changelog, and the shell completion scripts. -#### Binary +The binary is built with `cargo`, and the other artifacts are built `gen`, +located in `bin/gen`. -The binary is called `imdl`, and can be built with: +The binary can be built with: -``` -cargo build --release -``` + cargo build --release -After building, the binary will be present at `target/release/imdl`. +_`gen` requires [`help2man`](https://www.gnu.org/software/help2man/) to be +installed, which is used to generate man pages from subcommand `--help` +strings._ -#### Man Pages +The rest of the build artifacts can be built with `gen`: -Intermodal has a number of subcommands, each of which has a man page. The man -pages are generated from the `--help` text using -[`help2man`](https://www.gnu.org/software/help2man/). + cargo run --package gen -- --bin target/release/imdl all -To generate the man pages, ensure `help2man` is available, and run: +_The path to the built `imdl` executable should be passed to `gen` with the `--bin` flag._ -``` -mkdir -p man -cargo run --package gen man -``` +After running the above commands, the following table shows the location of the +built artifacts. -After building, the man pages will be available in `man`. - -#### Completion Scripts - -Completion scripts are available for a number of shells. To generate them, run: - -``` -mkdir -p completions -cargo run --release completions --dir completions -``` - -After running, the completion scripts will be available in `completions`. +| Artifact | Location | +|--------------------|----------------------------| +| Binary | `target/release/imdl` | +| Man Pages | `target/gen/man/*` | +| Completion Scripts | `target/gen/completions/*` | +| Changelog | `target/gen/CHANGELOG.md` | +| Readme | `target/gen/README.md` | ### Release Updates diff --git a/bin/package b/bin/package index 1c40748..d9368f5 100755 --- a/bin/package +++ b/bin/package @@ -6,7 +6,7 @@ version=${1#"refs/tags/"} os=$2 target=$3 src=`pwd` -dist=$src/dist +dist=$src/target/dist bin=imdl echo "Packaging $bin $version for $target..." @@ -38,32 +38,35 @@ case $os in esac echo "Building completions..." -rm -rf completions -mkdir completions -$executable completions --dir completions +cargo run --package gen -- --bin $executable completion-scripts echo "Generating changelog..." -cargo run --package gen changelog +cargo run --package gen -- --bin $executable changelog -echo "Copying release files..." -mkdir dist +echo "Generating readme..." +cargo run --package gen -- --bin $executable readme + +echo "Copying static files..." +mkdir $dist cp -r \ $executable \ - CHANGELOG.md \ CONTRIBUTING \ Cargo.lock \ Cargo.toml \ LICENSE \ - README.md \ - completions \ + $dist + +echo "Copying generated files..." +cp -r \ + target/gen/README.md \ + target/gen/CHANGELOG.md \ + target/gen/completions \ $dist if [[ $os != windows-latest ]]; then echo "Building man pages..." - rm -rf man - mkdir man - cargo run --package gen man - cp -r man $dist/man + cargo run --package gen -- --bin $executable man + cp -r target/gen/man $dist/man fi cd $dist diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md new file mode 120000 index 0000000..66fae34 --- /dev/null +++ b/book/src/SUMMARY.md @@ -0,0 +1 @@ +../../target/gen/book/SUMMARY.md \ No newline at end of file diff --git a/book/src/bittorrent.md b/book/src/bittorrent.md new file mode 120000 index 0000000..8eb7c79 --- /dev/null +++ b/book/src/bittorrent.md @@ -0,0 +1 @@ +../../target/gen/book/bittorrent.md \ No newline at end of file diff --git a/book/src/changelog.md b/book/src/changelog.md new file mode 120000 index 0000000..d4bb88d --- /dev/null +++ b/book/src/changelog.md @@ -0,0 +1 @@ +../../target/gen/book/changelog.md \ No newline at end of file diff --git a/book/src/commands b/book/src/commands new file mode 120000 index 0000000..af322da --- /dev/null +++ b/book/src/commands @@ -0,0 +1 @@ +../../target/gen/book/commands \ No newline at end of file diff --git a/book/src/commands.md b/book/src/commands.md new file mode 120000 index 0000000..e8312dd --- /dev/null +++ b/book/src/commands.md @@ -0,0 +1 @@ +../../target/gen/book/commands.md \ No newline at end of file diff --git a/book/src/faq.md b/book/src/faq.md new file mode 120000 index 0000000..f35918e --- /dev/null +++ b/book/src/faq.md @@ -0,0 +1 @@ +../../target/gen/book/faq.md \ No newline at end of file diff --git a/book/src/introduction.md b/book/src/introduction.md new file mode 120000 index 0000000..c588587 --- /dev/null +++ b/book/src/introduction.md @@ -0,0 +1 @@ +../../target/gen/book/introduction.md \ No newline at end of file diff --git a/book/src/references b/book/src/references new file mode 120000 index 0000000..0565169 --- /dev/null +++ b/book/src/references @@ -0,0 +1 @@ +../../target/gen/book/references \ No newline at end of file diff --git a/book/src/references.md b/book/src/references.md new file mode 120000 index 0000000..352e368 --- /dev/null +++ b/book/src/references.md @@ -0,0 +1 @@ +../../target/gen/book/references.md \ No newline at end of file diff --git a/justfile b/justfile index 7412534..8e83ea4 100644 --- a/justfile +++ b/justfile @@ -58,7 +58,8 @@ dev-deps: # update generated documentation gen: - cargo run --package gen all + cargo build + cargo run --package gen -- --bin target/debug/imdl all check-minimal-versions: ./bin/check-minimal-versions