A base project template for building small but reliable utilities in the Rust programming language.
NOTE: While the LICENSE
file must contain my preferred choice for
starting new projects (the GNU GPLv3), you may use the contents of this
repository under your choice of the MIT
and/or Apache 2.0 licenses.
- Uses StructOpt (with colorized
--help
output and "Did you mean...?" suggestions enabled) for argument parsing. - Uses error-chain for unified error handling.
- Presents an
app::main(opts: CliOpts) -> Result<()>
function to keep your application logic cleanly separated from argument parsing and handling of terminal errors. - Exposes Clap's support for generating shell completions by providing a
--dump-completions <shell>
option. - Enables almost all rustc and clippy lints without making clippy mandatory.
- A comprehensive set of just commands, easily
customized via variables (eg. for cross-compilation), including
install
anduninstall
, which also take care of shell completions and a manpage. just build-dist
for a 100% static i686 binary totalling roughly252KiB
(228KiB
withpanic="abort"
) in new projectsjust install-deps
to install all but two optional dependencies on Debian-family distros.just install-cargo-deps
to install all distro-agnostic dependencies.- A basic
.travis.yml
for use with Travis-CI and Nightli.es. - The
fmt
just command always calls the nightly version of rustfmt to ensure access to the excessive number of customization options which are gated away as unstable.
- Clone/download a copy of the boilerplate
- Run
apply.py path/to/new/project
- Edit
src/app.rs
to implement your application logic
The boilerplate is currently being refactored, but you should always get a
usable project skeleton by running apply.py
regardless of how much the
resulting skeleton may vary from revision to revision.
Metadata | |
---|---|
LICENSE |
A copy of the GNU GPLv3 as my "until I've had time to think about it" license of choice. You can replace this |
CONTRIBUTING.md |
A copy of the Developer Certificate of Origin, suitable for both this template and projects generated from it, which is the Linux kernel developers' more ideologically appropriate alternative to CLAs as a means of legally armoring themselves against bad-faith contributions |
Configuration | |
.gitignore |
Ignore /target and other generated files |
clippy.toml |
Whitelist for CamelCase names which trigger Clippy's "identifier needs backticks" lint |
rustfmt.toml |
A custom rustfmt configuration which shows
TODO /FIXME comments and attempts to make it conform
to the style I'm willing to enforce at the expense of not using rustfmt if
necessary. |
Development Automation | |
apply.py |
Run this to generate new projects as a workaround for cargo-generate's incompatibility with justfile syntax |
justfile |
Build/development-automation commands via just (a pure-Rust make-alike) |
Variable | Default Value | Description |
---|---|---|
CARGO_BUILD_TARGET |
i686-unknown-linux-musl |
The target for cargo commands to use and
install-rustup-deps to install |
build_flags |
--release |
An easy place to modify the build flags used |
channel |
stable |
An easy way to override the cargo channel for just this project |
features |
Extra cargo features to enable | |
build-dist | ||
sstrip_bin |
sstrip |
Set this if you need to override it for a cross-compiling sstrip |
strip_bin |
strip |
Set this to the cross-compiler's strip when cross-compiling |
strip_flags |
--strip-unneeded |
Flags passed to strip_bin |
upx_flags |
--ultra-brute |
Flags passed to UPX |
kcachegrind | ||
callgrind_args |
Extra arguments to pass to callgrind. | |
callgrind_out_file |
callgrind.out.justfile |
Temporary file used by just kcachegrind |
kcachegrind |
kcachegrind |
Set this to override how kcachegrind is called |
install and uninstall | ||
bash_completion_dir |
~/.bash_completion.d |
Where to install bash completions. You'll need to manually
add some lines to source these files in .bashrc. |
fish_completion_dir |
~/.config/fish/completions |
Where to install fish completions. You'll probably never need to
change this. |
manpage_dir |
~/.cargo/share/man/man1 |
Where to install manpages. As long as ~/.cargo/bin is
in your PATH , man should automatically pick up this
location. |
zsh_completion_dir |
~/.zsh/functions |
Where to install zsh completions. You'll need to add this
to your fpath manually |
Command | Arguments | Description |
---|---|---|
DEFAULT |
Shorthand for just test |
|
Development | ||
bloat |
args (optional) | Alias for cargo bloat |
check |
args (optional) | Alias for cargo check |
clean |
args (optional) | Superset of cargo clean -v which deletes other stuff this justfile
builds |
doc |
args (optional) | Run rustdoc with --document-private-items and then run
cargo-deadlinks |
fmt |
args (optional) | Alias for cargo +nightly fmt -- {{args}} |
fmt-check |
args (optional) | Alias for cargo +nightly fmt -- --check {{args}} which un-bloats
TODO/FIXME warnings |
kcachegrind |
args (optional) | Run a debug build under callgrind, then open the profile in KCachegrind |
kcov |
Generate a statement coverage report in target/cov/ |
|
test |
Run all installed static analysis, plus cargo test |
|
Local Builds | ||
build |
Alias for cargo build |
|
install |
Install the un-packed binary, shell completions, and a manpage | |
run |
args (optional) | Alias for cargo run -- {{args}} |
uninstall |
Remove any files installed by the install task (but leave any
parent directories created) |
|
Release Builds | ||
build-dist |
Call build and then strip and compress the resulting binary |
|
dist |
Call dist-supplemental and build-dist and copy the
packed binary to dist/ |
|
dist-supplemental |
Build the shell completions and a manpage, and put them in dist/ |
|
Dependencies | ||
install-apt-deps |
Use apt-get to install dependencies cargo can't
(except kcov and sstrip ) |
|
install-cargo-deps |
install-rustup-deps and then cargo install tools |
|
install-deps |
Run install-apt-deps and install-cargo-deps . List what
remains. |
|
install-rustup-deps |
Install (don't update) nightly and channel toolchains, plus
CARGO_BUILD_TARGET , clippy, and rustfmt |
-
Edit the
DEFAULT
command. That's what it's there for. -
You can use
just
from any subdirectory in your project. It's likegit
that way. -
just path/to/project/
(note the trailing slash) is equivalent to(cd path/to/project; just)
-
just path/to/project/command
is equivalent to(cd path/to/project; just command)
-
just install-cargo-deps
will installcargo-edit
so you can usecargo add
,cargo rm
, andcargo upgrade
to easily manage your dependencies. -
The simplest way to activate the bash completion installed by
just install
is to add this to your.bashrc
:for script in ~/.bash_completion.d/*; do . "$script" done
-
The simplest way to activate the zsh completion installed by
just install
is to add this to your.zshrc
:fpath=(~/.zsh/functions(:A) $fpath)
-
Only use Clap/StructOpt validators for references like filesystem paths (as opposed to self-contained data like set sizes) as a way to bail out early on bad data, not as your only check of validity. See this blog post for more.
In order to be as suitable as possible for building self-contained, high-reliability replacements for shell scripts, the following build options are defined:
- Backtrace support will be disabled in
error-chain
unless explicitly built with thebacktrace
feature. (This began as a workaround to unbreak cross-compiling to musl-libc and ARM after backtrace-rs 0.1.6 broke it, but it also makes sense to opt out of it if you're usingpanic="abort"
to save space)
- Unless otherwise noted, all optimizations listed above.
- Link-time optimization will be enabled (
lto = true
) - The binary will be built with
opt-level = "z"
to further reduce file size. - Optionally (uncomment a line in
Cargo.toml
) panic viaabort
rather than unwinding to allow backtrace code to be pruned away by dead code optimization.
- Unless otherwise noted, all optimizations listed above.
- The binary will be statically linked against musl-libc for maximum portability.
- The binary will be stripped with
--strip-unneeded
and then withsstrip
(a more aggressive companion used in embedded development) to produce the smallest possible pre-compression size. - The binary will be compressed via
upx --ultra-brute
. In my experience, this makes a file about 1/3rd the size of the input.
NOTE: --strip-unneeded
removes all symbols that readelf --syms
sees
from the just build
output, so it's not different from --strip-all
in
this case, but it's a good idea to get in the habit of using the safe option
that's smart enough to just Do What I Mean™.
- A packed binary will be built via
build-dist
and copied intodist/
- Shell completion files and a manpage will also be built and saved into
dist/
In order to use the full functionality offered by this boilerplate, the following dependencies must be installed:
just bloat
:- cargo-bloat
(
cargo install cargo-bloat
)
- cargo-bloat
(
just build-dist
:just fmt
andjust fmt-check
:- A nightly Rust toolchain
- (
rustup toolchain install nightly
) - rustfmt for the nightly toolchain
(
rustup component add rustfmt --toolchain nightly
)
just dist-supplemental
:- help2man
(
sudo apt-get install help2man
)
- help2man
(
just kcachegrind
:- Valgrind (
sudo apt-get install valgrind
) - KCachegrind (
sudo apt-get install kcachegrind
)
- Valgrind (
just kcov
:- A Rust-compatible build of kcov
just test
:- clippy
(
rustup component add clippy
) - cargo-deadlinks
(
cargo install cargo-deadlinks
) - cargo-outdated
(
cargo install cargo-outdated
)
- clippy
(
-
Debian/Ubuntu/Mint:
export PATH="$HOME/.cargo/bin:$PATH" cargo install just just install-deps # ...and now manually install the following optional tools: # - sstrip (from ELFkickers) # - kcov (version 31 or higher with --verify support)
-
Other distros:
export PATH="$HOME/.cargo/bin:$PATH" cargo install just just install-cargo-deps # ...and now manually install the following optional tools: # - help2man # - kcachegrind # - kcov (version 31 or higher with --verify support) # - strip (from binutils) # - sstrip (from ELFkickers) # - upx # - valgrind
- Get a feel for the workflow surrounding building a project with Failure and decide whether to rebase this template on top of it.
- Investigate how flexible QuiCLI and
its dependency on env_logger are and whether it'd be useful to rebase on it
or whether I'd just be reinventing most of it anyway to force the exact
look and feel I achieved with stderrlog.
(eg. The
Verbosity
struct doesn't implement "-v
and-q
are mirrors of each other" and I'm rather fond of stderrlog's approach to timestamp toggling.)- What effect does quicli have on the final binary size? (not a huge concern)
- Investigate why cargo-cov isn't hiding the components of the rust standard library and whether it can be induced to generate coverage despite some tests failing. If so, add a command for it.
- Figure out whether StructOpt or Clap is to blame for doubling the leading
newline when
about
is specified via the doc comment and then report the bug. - Read the callgrind docs and
figure out how to exclude the Rust standard library from what KCacheGrind
displays.
- I may need to filter the output. [1]
- Figure out how to add a
just
task for a faster but less precise profiler like gprof [1] [2], OProfile [1], or perf [1] to make it easy to leverage the various trade-offs. (And make sure to provide convenient access to flame graphs and at least one perf inspector GUI or TUI.) - Include a reference to this blog post on how profilers can can mislead in different ways and probably also this too.
- Look into options for making it as easy as possible to optimize and regression-test runtime performance. [1] [2] [3] [4]
- Test and enhance
.travis.yml
- Consider officially supporting Windows as a target (probably using
cargo-make instead of Just) and, if
I do, come up with an
appveyor.yml
... possibly the one from this project: https://github.com/starkat99/appveyor-rust
- Consider officially supporting Windows as a target (probably using
cargo-make instead of Just) and, if
I do, come up with an
- Add a
run-memstats
Just task which swaps in jemalloc and sets MALLOC_CONF=stats_print:true - Investigate commit hooks [1] [2] [3]
- Once I've cleared out these TODOs, consider using this space for a reminder list of best practices for avoiding "higher-level footguns" noted in my pile of assorted advice. (Things like "If you can find a way to not need path manipulation beyond 'pass this opaque token around', then you can eliminate entire classes of bugs")
- At least list a snip of example code for something like RustyLine as the suggested way to do simple user prompting.
- Gather my custom clap validators into a crate, add some more, and have this
depend on it:
- Self-Contained data:
- Boolean is
1
/y
/yes
/t
/true
or0
/n
/no
/f
/false
(case-insensitive, include a utility function for actual parsing) - Integers:
- Can be parsed as a decimal integer
> 0
(eg. number of volumes) - Can be parsed as a decimal integer
>= 0
(eg. number of bytes) - Number of bytes, with optional SI mebi- unit suffix
(eg.
16m
, including optionalb
, case-insensitive)
- Can be parsed as a decimal integer
- Floats:
- Can be parsed as a float in the range
0.0 <= x <= 1.0
- Can be parsed as a float in the range
- Boolean is
- Invalidatable/Referential data:
- Input files:
- File exists and is readable
- Directory exists and is browsable (
+rX
) - Path is a readable file or browsable directory (ie. read or recurse)
- Output files:
- Integers:
- Augmented "number of bytes, with optional SI mebi- unit suffix" validator with upper limit for producing files representable by ISO9660/FAT32 filesystems on removable media. (2GiB, since some implementations use 32-bit signed offsets)
- Strings:
- Is valid FAT32-safe filename/prefix (path separators disallowed)
- Paths:
- File path is probably FAT32 writable
- If file exists,
access()
says it's probably writable - If file does not exist, name is FAT32-valid and within a probably-writable directory.
- If file exists,
- File path is probably FAT32 writable, with
mkdir -p
- Nonexistent path components are FAT32-valid
- Closest existing ancestor is a probably-writable directory
- Directory exists and is probably writable
- "probably writable" is tested via
access()
and will need portability shimming.
- "probably writable" is tested via
- File path is probably FAT32 writable
- Integers:
- Network I/O:
- Integers:
- Successfully parses into a valid listening TCP/UDP port number (0-65535, I think)
- Successfully parses into a valid, non-root, listening TCP/UDP port number (0 or 1024-65535, I think)
- Successfully parses into a valid connecting TCP/UDP port number (1-65535, I think)
- Strings:
- Successfully parses into a
SocketAddr
(IP+port, may perform DNS lookup?) - Successfully parses into an
IpAddr
(may perform DNS lookup?)
- Successfully parses into a
- URLs:
- Is well-formed relative URL (external dependency behind a cargo feature)
- Is well-formed absolute URL (external dependency behind a cargo feature)
- Integers:
- Input files:
- Self-Contained data: