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
This commit is contained in:
Casey Rodarmor 2020-04-30 21:21:20 -07:00
parent bf29d74b3e
commit e7872f56f2
No known key found for this signature in database
GPG Key ID: 556186B153EC6FE0
28 changed files with 503 additions and 547 deletions

View File

@ -108,7 +108,7 @@ jobs:
if: matrix.os == 'macos-latest' if: matrix.os == 'macos-latest'
run: | run: |
brew install help2man brew install help2man
cargo run --package gen all cargo run --package gen -- --bin target/debug/imdl all
git diff --no-ext-diff --exit-code git diff --no-ext-diff --exit-code
- name: Install `mdbook` - name: Install `mdbook`
@ -118,7 +118,7 @@ jobs:
- name: Build Book - name: Build Book
run: | run: |
cargo run --package gen book cargo run --package gen -- --bin target/debug/imdl book
mdbook build book --dest-dir ../www/book mdbook build book --dest-dir ../www/book
- name: Record Git Revision - name: Record Git Revision
@ -139,12 +139,12 @@ jobs:
shell: bash shell: bash
run: ./bin/package ${{github.ref}} ${{matrix.os}} ${{matrix.target}} run: ./bin/package ${{github.ref}} ${{matrix.os}} ${{matrix.target}}
- name: Publish - name: Publish Release Archive
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
with: with:
draft: false draft: false
files: ${{steps.package.outputs.archive}} files: ${{steps.package.outputs.archive}}
prerelease: false prerelease: true
env: env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}

12
.gitignore vendored
View File

@ -1,17 +1,5 @@
**/*.rs.bk **/*.rs.bk
/CHANGELOG.md
/book/book /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 /target
/wiki /wiki
/www/book /www/book

10
Cargo.lock generated
View File

@ -354,6 +354,12 @@ version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3" checksum = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3"
[[package]]
name = "fs_extra"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f2a4a2034423744d2cc7ca2068453168dcdb82c438419e639a26bd87839c674"
[[package]] [[package]]
name = "gen" name = "gen"
version = "0.0.0" version = "0.0.0"
@ -363,10 +369,9 @@ dependencies = [
"cargo_toml", "cargo_toml",
"chrono", "chrono",
"fehler", "fehler",
"fs_extra",
"git2", "git2",
"globset", "globset",
"ignore",
"lexiclean",
"libc", "libc",
"log", "log",
"pretty_env_logger", "pretty_env_logger",
@ -379,7 +384,6 @@ dependencies = [
"strum_macros", "strum_macros",
"tempfile", "tempfile",
"url", "url",
"walkdir",
] ]
[[package]] [[package]]

View File

@ -31,10 +31,7 @@ For more about the project and its goals, check out
- [Examples](#examples) - [Examples](#examples)
- [FAQ](#faq) - [FAQ](#faq)
- [Notes for Packagers](#notes-for-packagers) - [Notes for Packagers](#notes-for-packagers)
- [Package Artifacts](#package-artifacts) - [Build Artifacts](#build-artifacts)
- [Binary](#binary)
- [Man Pages](#man-pages)
- [Completion Scripts](#completion-scripts)
- [Release Updates](#release-updates) - [Release Updates](#release-updates)
- [Chat](#chat) - [Chat](#chat)
- [Contributing](#contributing) - [Contributing](#contributing)
@ -181,46 +178,38 @@ Intermodal is distributed under the
a public domain dedication with a fallback all-permissive license. The SPDX 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). 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 There are a number of build artifacts: the binary, the man pages, the
shell completion scripts. 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 cargo run --package gen -- --bin target/release/imdl all
pages are generated from the `--help` text using
[`help2man`](https://www.gnu.org/software/help2man/).
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._
``` After running the above commands, the following table shows the location of the
mkdir -p man built artifacts.
cargo run --package gen man
```
After building, the man pages will be available in `man`. | Artifact | Location |
|--------------------|----------------------------|
#### Completion Scripts | Binary | `target/release/imdl` |
| Man Pages | `target/gen/man/*` |
Completion scripts are available for a number of shells. To generate them, run: | Completion Scripts | `target/gen/completions/*` |
| Changelog | `target/gen/CHANGELOG.md` |
``` | Readme | `target/gen/README.md` |
mkdir -p completions
cargo run --release completions --dir completions
```
After running, the completion scripts will be available in `completions`.
### Release Updates ### Release Updates

View File

@ -11,10 +11,9 @@ askama = "0.9.0"
cargo_toml = "0.8.0" cargo_toml = "0.8.0"
chrono = "0.4.11" chrono = "0.4.11"
fehler = "1.0.0" fehler = "1.0.0"
fs_extra = "1.1.0"
git2 = "0.13.1" git2 = "0.13.1"
globset = "0.4.5" globset = "0.4.5"
ignore = "0.4.14"
lexiclean = "0.0.1"
libc = "0.2.69" libc = "0.2.69"
log = "0.4.8" log = "0.4.8"
pretty_env_logger = "0.4.0" pretty_env_logger = "0.4.0"
@ -25,7 +24,6 @@ structopt = "0.3.12"
strum = "0.18.0" strum = "0.18.0"
strum_macros = "0.18.0" strum_macros = "0.18.0"
tempfile = "3.1.0" tempfile = "3.1.0"
walkdir = "2.3.1"
[dependencies.serde] [dependencies.serde]
version = "1.0.106" version = "1.0.106"

16
bin/gen/src/arguments.rs Normal file
View File

@ -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)?;
}
}

View File

@ -2,7 +2,7 @@ use crate::common::*;
pub(crate) struct Bin { pub(crate) struct Bin {
path: PathBuf, path: PathBuf,
pub(crate) subcommands: Vec<Subcommand>, pub(crate) subcommands: Vec<BinSubcommand>,
} }
impl Bin { impl Bin {
@ -22,7 +22,7 @@ impl Bin {
#[throws] #[throws]
fn add_subcommands(&mut self, command: &mut Vec<String>) { fn add_subcommands(&mut self, command: &mut Vec<String>) {
let subcommand = Subcommand::new(&self.path, command.clone())?; let subcommand = BinSubcommand::new(&self.path, command.clone())?;
for name in &subcommand.subcommands { for name in &subcommand.subcommands {
command.push(name.into()); command.push(name.into());

View File

@ -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<String>,
pub(crate) subcommands: Vec<String>,
}
impl BinSubcommand {
#[throws]
pub(crate) fn new(bin: &Path, command: Vec<String>) -> 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)
}
}

View File

@ -17,8 +17,6 @@ pub(crate) use cargo_toml::Manifest;
pub(crate) use chrono::{DateTime, NaiveDateTime, Utc}; pub(crate) use chrono::{DateTime, NaiveDateTime, Utc};
pub(crate) use fehler::{throw, throws}; pub(crate) use fehler::{throw, throws};
pub(crate) use git2::{Commit, Oid, Repository}; 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 libc::EXIT_FAILURE;
pub(crate) use log::info; pub(crate) use log::info;
pub(crate) use regex::Regex; pub(crate) use regex::Regex;
@ -29,7 +27,6 @@ pub(crate) use structopt::StructOpt;
pub(crate) use strum::VariantNames; pub(crate) use strum::VariantNames;
pub(crate) use strum_macros::{EnumVariantNames, IntoStaticStr}; pub(crate) use strum_macros::{EnumVariantNames, IntoStaticStr};
pub(crate) use url::Url; pub(crate) use url::Url;
pub(crate) use walkdir::WalkDir;
// modules // modules
pub(crate) use crate::error; 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 // structs and enums
pub(crate) use crate::{ pub(crate) use crate::{
bin::Bin, changelog::Changelog, config::Config, entry::Entry, error::Error, example::Example, arguments::Arguments, bin::Bin, bin_subcommand::BinSubcommand, changelog::Changelog,
faq::Faq, faq_entry::FaqEntry, introduction::Introduction, kind::Kind, metadata::Metadata, config::Config, entry::Entry, error::Error, example::Example, faq::Faq, faq_entry::FaqEntry,
opt::Opt, package::Package, project::Project, readme::Readme, reference::Reference, introduction::Introduction, kind::Kind, metadata::Metadata, options::Options, package::Package,
reference_section::ReferenceSection, release::Release, subcommand::Subcommand, summary::Summary, project::Project, readme::Readme, reference::Reference, reference_section::ReferenceSection,
table::Table, release::Release, subcommand::Subcommand, summary::Summary, table::Table,
}; };

View File

@ -58,6 +58,12 @@ pub(crate) enum Error {
dst: PathBuf, dst: PathBuf,
source: io::Error, 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))] #[snafu(display("Git error: {}", source))]
Git { source: git2::Error }, Git { source: git2::Error },
#[snafu(display("Regex compilation error: {}", source))] #[snafu(display("Regex compilation error: {}", source))]
@ -73,10 +79,6 @@ pub(crate) enum Error {
TemplateRender { source: askama::Error }, TemplateRender { source: askama::Error },
#[snafu(display("Failed to get workdir for repo at `{}`", repo.display()))] #[snafu(display("Failed to get workdir for repo at `{}`", repo.display()))]
Workdir { repo: PathBuf }, 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))] #[snafu(display("Failed to strip path prefix: {}", source))]
StripPrefix { source: StripPrefixError }, StripPrefix { source: StripPrefixError },
} }
@ -98,15 +100,3 @@ impl From<cargo_toml::Error> for Error {
Self::CargoToml { source } Self::CargoToml { source }
} }
} }
impl From<ignore::Error> for Error {
fn from(source: ignore::Error) -> Self {
Self::Ignore { source }
}
}
impl From<walkdir::Error> for Error {
fn from(source: walkdir::Error) -> Self {
Self::Walkdir { source }
}
}

View File

@ -3,7 +3,9 @@ use crate::common::*;
#[macro_use] #[macro_use]
mod cmd; mod cmd;
mod arguments;
mod bin; mod bin;
mod bin_subcommand;
mod changelog; mod changelog;
mod command_ext; mod command_ext;
mod common; mod common;
@ -16,7 +18,7 @@ mod faq_entry;
mod introduction; mod introduction;
mod kind; mod kind;
mod metadata; mod metadata;
mod opt; mod options;
mod package; mod package;
mod project; mod project;
mod readme; mod readme;
@ -33,7 +35,7 @@ mod template_ext;
fn main() { fn main() {
pretty_env_logger::init(); 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 bold = Style::new().bold();
let red = Style::new().fg(ansi_term::Color::Red).bold(); let red = Style::new().fg(ansi_term::Color::Red).bold();
eprintln!("{}: {}", red.paint("error"), bold.paint(error.to_string())); eprintln!("{}: {}", red.paint("error"), bold.paint(error.to_string()));

View File

@ -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<Path>, 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<Path>) {
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 })?;
}
}
}

11
bin/gen/src/options.rs Normal file
View File

@ -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,
}

View File

@ -5,11 +5,12 @@ pub(crate) struct Project {
pub(crate) root: PathBuf, pub(crate) root: PathBuf,
pub(crate) config: Config, pub(crate) config: Config,
pub(crate) bin: Bin, pub(crate) bin: Bin,
pub(crate) executable: PathBuf,
} }
impl Project { impl Project {
#[throws] #[throws]
pub(crate) fn load() -> Self { pub(crate) fn load(options: &Options) -> Self {
let start_dir = env::current_dir().context(error::CurrentDir)?; let start_dir = env::current_dir().context(error::CurrentDir)?;
let repo = Repository::discover(&start_dir).context(error::RepositoryDiscover { start_dir })?; let repo = Repository::discover(&start_dir).context(error::RepositoryDiscover { start_dir })?;
@ -23,7 +24,7 @@ impl Project {
let config = Config::load(&root)?; let config = Config::load(&root)?;
let bin = Bin::new(&root.join("target/debug/imdl"))?; let bin = Bin::new(&options.bin)?;
let example_commands = config let example_commands = config
.examples .examples
@ -45,10 +46,22 @@ impl Project {
} }
Project { Project {
executable: options.bin.clone(),
bin,
config,
repo, repo,
root, 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
}
} }

View File

@ -9,6 +9,6 @@ pub(crate) struct ReferenceSection {
impl ReferenceSection { impl ReferenceSection {
pub(crate) fn path(&self) -> String { pub(crate) fn path(&self) -> String {
format!("./references/{}.md", self.title.slug()) format!("references/{}.md", self.title.slug())
} }
} }

View File

@ -1,164 +1,240 @@
use crate::common::*; use crate::common::*;
#[derive(Debug, Ord, PartialOrd, Eq, PartialEq)] #[derive(StructOpt)]
pub(crate) struct Subcommand { pub(crate) enum Subcommand {
pub(crate) bin: PathBuf, #[structopt(about("Update all generated docs"))]
pub(crate) command: Vec<String>, All,
pub(crate) subcommands: Vec<String>, #[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<Path>, 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<Path>) {
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 { impl Subcommand {
#[throws] #[throws]
pub(crate) fn new(bin: &Path, command: Vec<String>) -> Self { pub(crate) fn run(self, options: Options) {
let wide_help = Command::new(bin) let project = Project::load(&options)?;
.args(command.as_slice())
.env("IMDL_TERM_WIDTH", "200")
.arg("--help")
.out()?;
const MARKER: &str = "\nSUBCOMMANDS:\n"; match self {
Self::Changelog => Self::changelog(&project)?,
let mut subcommands = Vec::new(); Self::CommitTemplate => {
println!("{}", Metadata::default().to_string());
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::CommitTypes => {
for kind in Kind::VARIANTS {
Self { println!("{}", kind)
bin: bin.into(), }
command, }
subcommands, 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] #[throws]
fn help(&self) -> String { pub(crate) fn all(project: &Project) {
info!("Getting help for `{}`", self.command_line()); Self::changelog(&project)?;
Self::completion_scripts(&project)?;
Command::new(&self.bin) Self::readme(&project)?;
.args(self.command.as_slice()) Self::book(&project)?;
.env("IMDL_TERM_WIDTH", "80") Self::man(&project)?;
.arg("--help")
.out()?
} }
#[throws] #[throws]
pub(crate) fn man(&self) -> String { pub(crate) fn changelog(project: &Project) {
let command_line = self.command_line(); 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() { clean_dir(&completions)?;
"A 40' shipping container for the Internet".to_string()
} else {
help.lines().nth(1).unwrap().into()
};
let include = format!( cmd!(&project.executable, "completions", "--dir", completions).status_into_result()?
"\ }
[NAME]
\\fB{}\\fR
- {}
",
name, description
);
#[throws]
pub(crate) fn diff(project: &Project) {
let tmp = tempfile::tempdir().context(error::Tempdir)?; 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 { fs::remove_dir_all(src).context(error::Filesystem { path: src })?;
path: &include_path,
})?;
let version = cmd!(&self.bin, "--version") cmd!("cargo", "run", "--package", "gen", "all").status_into_result()?;
.out()?
.split_whitespace()
.nth(1)
.unwrap()
.to_owned();
info!("Running help2man for `{}`", command_line); let dir = tmp.path().join(name);
let mut command = self.bin.as_os_str().to_owned(); fs::create_dir(&dir).context(error::Filesystem { path: &dir })?;
for arg in &self.command {
command.push(" ");
command.push(arg);
}
let output = cmd!( fs_extra::dir::copy(src, &dir, &fs_extra::dir::CopyOptions::new())
"help2man", .context(error::FilesystemRecursiveCopy { src, dst: dir })?;
"--include",
&include_path, Ok(())
"--manual", };
"Intermodal Manual",
"--no-info", const HEAD: &str = "HEAD";
"--source",
&format!("Intermodal {}", version), gen(HEAD)?;
command
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()?; .status_into_result()?;
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
} }
#[throws] #[throws]
pub(crate) fn page(&self) -> String { pub(crate) fn readme(project: &Project) {
let help = self.help()?; info!("Generating readme…");
format!("# `{}`\n```\n{}\n```", self.command_line(), help)
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 })?;
}
} }
} }

View File

@ -154,46 +154,38 @@ Intermodal is distributed under the
a public domain dedication with a fallback all-permissive license. The SPDX 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). 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 There are a number of build artifacts: the binary, the man pages, the
shell completion scripts. 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 cargo run --package gen -- --bin target/release/imdl all
pages are generated from the `--help` text using
[`help2man`](https://www.gnu.org/software/help2man/).
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._
``` After running the above commands, the following table shows the location of the
mkdir -p man built artifacts.
cargo run --package gen man
```
After building, the man pages will be available in `man`. | Artifact | Location |
|--------------------|----------------------------|
#### Completion Scripts | Binary | `target/release/imdl` |
| Man Pages | `target/gen/man/*` |
Completion scripts are available for a number of shells. To generate them, run: | Completion Scripts | `target/gen/completions/*` |
| Changelog | `target/gen/CHANGELOG.md` |
``` | Readme | `target/gen/README.md` |
mkdir -p completions
cargo run --release completions --dir completions
```
After running, the completion scripts will be available in `completions`.
### Release Updates ### Release Updates

View File

@ -6,7 +6,7 @@ version=${1#"refs/tags/"}
os=$2 os=$2
target=$3 target=$3
src=`pwd` src=`pwd`
dist=$src/dist dist=$src/target/dist
bin=imdl bin=imdl
echo "Packaging $bin $version for $target..." echo "Packaging $bin $version for $target..."
@ -38,32 +38,35 @@ case $os in
esac esac
echo "Building completions..." echo "Building completions..."
rm -rf completions cargo run --package gen -- --bin $executable completion-scripts
mkdir completions
$executable completions --dir completions
echo "Generating changelog..." echo "Generating changelog..."
cargo run --package gen changelog cargo run --package gen -- --bin $executable changelog
echo "Copying release files..." echo "Generating readme..."
mkdir dist cargo run --package gen -- --bin $executable readme
echo "Copying static files..."
mkdir $dist
cp -r \ cp -r \
$executable \ $executable \
CHANGELOG.md \
CONTRIBUTING \ CONTRIBUTING \
Cargo.lock \ Cargo.lock \
Cargo.toml \ Cargo.toml \
LICENSE \ LICENSE \
README.md \ $dist
completions \
echo "Copying generated files..."
cp -r \
target/gen/README.md \
target/gen/CHANGELOG.md \
target/gen/completions \
$dist $dist
if [[ $os != windows-latest ]]; then if [[ $os != windows-latest ]]; then
echo "Building man pages..." echo "Building man pages..."
rm -rf man cargo run --package gen -- --bin $executable man
mkdir man cp -r target/gen/man $dist/man
cargo run --package gen man
cp -r man $dist/man
fi fi
cd $dist cd $dist

1
book/src/SUMMARY.md Symbolic link
View File

@ -0,0 +1 @@
../../target/gen/book/SUMMARY.md

1
book/src/bittorrent.md Symbolic link
View File

@ -0,0 +1 @@
../../target/gen/book/bittorrent.md

1
book/src/changelog.md Symbolic link
View File

@ -0,0 +1 @@
../../target/gen/book/changelog.md

1
book/src/commands Symbolic link
View File

@ -0,0 +1 @@
../../target/gen/book/commands

1
book/src/commands.md Symbolic link
View File

@ -0,0 +1 @@
../../target/gen/book/commands.md

1
book/src/faq.md Symbolic link
View File

@ -0,0 +1 @@
../../target/gen/book/faq.md

1
book/src/introduction.md Symbolic link
View File

@ -0,0 +1 @@
../../target/gen/book/introduction.md

1
book/src/references Symbolic link
View File

@ -0,0 +1 @@
../../target/gen/book/references

1
book/src/references.md Symbolic link
View File

@ -0,0 +1 @@
../../target/gen/book/references.md

View File

@ -58,7 +58,8 @@ dev-deps:
# update generated documentation # update generated documentation
gen: gen:
cargo run --package gen all cargo build
cargo run --package gen -- --bin target/debug/imdl all
check-minimal-versions: check-minimal-versions:
./bin/check-minimal-versions ./bin/check-minimal-versions