improved error messages and began documentation

This commit is contained in:
Noah Swerhun 2024-03-05 23:54:20 -06:00
parent 19be0d2a6a
commit 338cc3c794
6 changed files with 175 additions and 167 deletions

128
Cargo.lock generated
View file

@ -2,54 +2,12 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 3
[[package]]
name = "anstream"
version = "0.6.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"utf8parse",
]
[[package]] [[package]]
name = "anstyle" name = "anstyle"
version = "1.0.6" 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 = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc"
[[package]]
name = "anstyle-parse"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7"
dependencies = [
"anstyle",
"windows-sys",
]
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.1" version = "4.5.1"
@ -66,10 +24,8 @@ version = "4.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f3e7391dad68afb0c2ede1bf619f579a3dc9c2ec67f089baa397123a2f3d1eb" checksum = "9f3e7391dad68afb0c2ede1bf619f579a3dc9c2ec67f089baa397123a2f3d1eb"
dependencies = [ dependencies = [
"anstream",
"anstyle", "anstyle",
"clap_lex", "clap_lex",
"strsim",
] ]
[[package]] [[package]]
@ -90,12 +46,6 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
[[package]]
name = "colorchoice"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]] [[package]]
name = "equivalent" name = "equivalent"
version = "1.0.1" version = "1.0.1"
@ -185,12 +135,6 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "strsim"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01"
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.52" version = "2.0.52"
@ -242,78 +186,6 @@ version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "utf8parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675"
[[package]]
name = "windows_i686_gnu"
version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3"
[[package]]
name = "windows_i686_msvc"
version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8"
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.6.5" version = "0.6.5"

View file

@ -6,5 +6,5 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
clap = { version = "4.5.1", features = ["derive"] } clap = { version = "4.5.1", features = ["derive", "std", "help", "usage"], default-features = false }
toml = "0.8.10" toml = "0.8.10"

148
README.md
View file

@ -21,13 +21,14 @@ that any experienced programmer should be familiar with. In doing so, ngen
fills the gap between writing your own makefile and wrangling with fills the gap between writing your own makefile and wrangling with
CMakeLists.txt. CMakeLists.txt.
ngen generates files for the [Ninja](https://ninja-build.org/) build system. ngen generates files for the small and modern [Ninja](https://ninja-build.org/)
Ninja is a small, modern build system that uses a bare-bones configuration build system. "Where other build systems are high-level languages Ninja aims to
language that is easy to both read and generate (but not necessarily easy to be an assembler," according to Ninja's website. It can be thought of as a
write by hand). "Where other build systems are high-level languages Ninja aims
to be an assembler," according to Ninja's website. It can be thought of as a
simpler, faster replacement for the classic `make`. It is used by default by simpler, faster replacement for the classic `make`. It is used by default by
Meson; CMake can also be configured to use ninja as a backend. Meson; CMake can also be configured to use ninja as a backend. Ninja is used by
used by Google to build Chromium, v8, etc., and it is also used to build LLVM.
(All this to say, Ninja is commonly used and it is likely installed on your
system already.)
## Building ## Building
@ -36,3 +37,138 @@ Build: `cargo build --release`
Install: `sudo sh install.sh` Install: `sudo sh install.sh`
Uninstall: `sudo sh uninstall.sh` Uninstall: `sudo sh uninstall.sh`
## Usage
What follows is a tutorial of how to set up ngen for an existing executable
project. If you are looking for a reference, look <SOMEWHERE>.
The first thing you will need is an `ngen.toml` file. This is what will specify
all of the build parameters, such as compilation flags, source files to track,
etc. Start by creating this file and opening it in your editor.
The simplest valid `ngen.toml` just lists the source files you want to track.
Lets say you have a executable project that looks like this:
```txt
example
├── ngen.toml
└── src
├── foobar.c
├── functions.c
├── include
│   ├── foobar.h
│   ├── functions.h
│   ├── main.h
│   └── util.h
├── main.c
└── util.c
```
In your `ngen.toml`, write the following:
```toml
sources = [
"src/main.c",
"src/util.c",
"src/functions.c",
"src/foobar.c",
]
```
**The `sources` key is a *list* of *strings*, each specifying a single source
file name.**
Now run `ngen`. This will generate a `build.ninja` file in the current
working directory. You won't ever have to touch file; thats what ngen is for. You
also won't ever have to run `ngen` yourself again (unless your `build.ninja`
gets deleted); Ninja will take care of regenerating the build file if
`ngen.toml` changes.
With your `build.ninja` generated, run `ninja` on the command line. Thats it!
Your project is now built, you will find it in `build/main/a.out`. Remember, you
can also freely add and remove files from this list without running `ngen`
again: Ninja will regenerate the `build.ninja` for you.
Now, while this is functional, it isn't very useful. It is very likely that you
will want to specify a compiler (gcc/clang), pass some flags, link some
libraries into your final exectuable, and definitly name your program something
other than "a.out." ngen makes these things dead simple, too.
**The `outfile` key is a *string* that specifies the name of the file produced
by the `linker` (see below).**
Lets set this to "example."
```toml
outfile = "example"
```
**The `compiler` key is a *string* that specifies the program that will be used
to turn .c files into .o files.**
By default, if not specified, `compiler` is set to "cc," which on Linux systems
should be a C compiler. It should be noted that the compiler you choose must
support the `-MD` and `-MF` flags to generate dependency files (both gcc and
clang support this). Lets say we want to use "gcc." Add the following line to
your `ngen.toml`:
```toml
compiler = "gcc"
```
**The `compiler_flags` key is a *list* of *strings* that contains the arguments
to be passed to the `compiler` during the compilation of each `source` file.**
It is not necessary to add the `-c` or `-o outfile` flags, ngen will take care
of this for you. For example, add the following to your `ngen.toml`:
```toml
compiler_flags = ["-Wall", "-Wextra -O2"]
```
**The `linker` key is a *string* that specifies the program that will be used
to combine the .o files into the final `outfile`.**
If the `linker` key is not found, it will be set to the value of `compiler`. For
this example, we don't have to change anything here.
**The `linker_flags` key is a *list* of *strings* that contains the arguments
to be passed to the `linker` during the linking of the `outfile`.**
Library flags (`-lm`, `lyourlib`) should NOT be included here. This is for
linker options, not libraries. There is nothing we have to set here; the sytax
works the same as `compiler_flags`.
**The `linker_libs` key is a *list* of *strings* that contains the link library
arguments to be linked to the `outfile`.**
THIS is where library flags (`-lm`, `lyourlib`) go. Lets say our example project
needs the math library:
```toml
linker_libs = ["-lm"]
```
Now, our `ngen.toml` looks like this:
```toml
outfile = "example"
compiler = "gcc"
compiler_flags = ["-Wall", "-Wextra -O2"]
linker_libs = ["-lm"]
sources = [
"src/main.c",
"src/util.c",
"src/functions.c",
"src/foobar.c",
]
```
This is a much more realistic looking project. Once again, any changes to any of
these values will be automatically picked up by Ninja and accounted for in the
build. Running `ninja -v` should show that the options you set were recognized,
and your files were rebuilt accordingly.
TODO: explain
- seperate targets
- config table

View file

@ -1,5 +1,3 @@
use core::panic;
use toml::{Table, Value}; use toml::{Table, Value};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -24,7 +22,7 @@ impl Config {
if let Value::String(s) = table.get(k).unwrap() { if let Value::String(s) = table.get(k).unwrap() {
self.build_dir = s.to_owned(); self.build_dir = s.to_owned();
} else { } else {
panic!("fatal: {k} invalid type, must be a string."); panic!("error parsing config table: {k} invalid type, must be a string.");
} }
} }
"compile_commands" => { "compile_commands" => {
@ -32,17 +30,17 @@ impl Config {
//eprintln!("warn: config.compile_commands has no functionality (yet)"); //eprintln!("warn: config.compile_commands has no functionality (yet)");
self.compile_commands = *b; self.compile_commands = *b;
} else { } else {
panic!("fatal: {k} invalid type, must be a boolean."); panic!("error parsing config table: {k} invalid type, must be a boolean.");
} }
} }
"compile_commands_target" => { "compile_commands_target" => {
if let Value::String(s) = table.get(k).unwrap() { if let Value::String(s) = table.get(k).unwrap() {
self.compile_commands_target = s.to_owned(); self.compile_commands_target = s.to_owned();
} else { } else {
panic!("fatal: {k} invalid type, must be a string."); panic!("error parsing config table: {k} invalid type, must be a string.");
} }
} }
_ => panic!("fatal: unrecognized key {k}."), _ => panic!("error parsing config table: unrecognized key {k}."),
} }
} }
} }

View file

@ -1,7 +1,6 @@
use clap::Parser; use clap::Parser;
use core::panic;
use std::{ use std::{
fs::{read, write}, fs::{read_to_string, write},
path::Path, path::Path,
}; };
use toml::{Table, Value}; use toml::{Table, Value};
@ -12,15 +11,18 @@ use target::*;
mod config; mod config;
use config::*; use config::*;
/// ngen is a build file generator for the ninja build system.
#[derive(Parser)] #[derive(Parser)]
struct Args { struct Args {
/// Path to the toml configuration file
#[arg(short, long, default_value = "ngen.toml")] #[arg(short, long, default_value = "ngen.toml")]
config_file: String, config_file: String,
/// Path to generated ninja file
#[arg(short, long, default_value = "build.ninja")] #[arg(short, long, default_value = "build.ninja")]
output_file: String, output_file: String,
#[arg(long)] #[arg(long, hide = true)]
write_compile_commands: Option<String>, write_compile_commands: Option<String>,
} }
@ -90,7 +92,7 @@ fn gen_compile_commands(target: &Target, build_dir: &str) -> String {
let pwd = Path::new("."); let pwd = Path::new(".");
let pwd = pwd let pwd = pwd
.canonicalize() .canonicalize()
.expect("cwd is non canonical (something is very wrong)"); .unwrap_or_else(|e| panic!("cwd is non canonical (something is very wrong): {e}"));
let pwd = pwd.to_str().unwrap(); let pwd = pwd.to_str().unwrap();
for (i, s) in target.sources.iter().enumerate() { for (i, s) in target.sources.iter().enumerate() {
@ -120,11 +122,10 @@ fn main() {
let output_file = &args.output_file; let output_file = &args.output_file;
let config_file_name = &args.config_file; let config_file_name = &args.config_file;
let config_file_raw = let config_file = read_to_string(config_file_name)
read(config_file_name).unwrap_or_else(|e| panic!("fatal: could not read file: {e}")); .unwrap_or_else(|e| panic!("could not read config file {config_file_name}: {e}"));
let config_file_string = String::from_utf8_lossy(&config_file_raw); let parsed_config = toml::from_str::<Table>(&config_file)
let parsed_config = toml::from_str::<Table>(&config_file_string) .unwrap_or_else(|e| panic!("error parsing toml file {config_file_name}: {e}"));
.unwrap_or_else(|e| panic!("fatal: could not parse config file: {e}"));
// get config // get config
let mut config = Config::get_default(); let mut config = Config::get_default();
@ -174,10 +175,10 @@ fn main() {
); );
} }
write( write(
file, &file,
&gen_compile_commands(&target_for_comp_cmds, &config.build_dir), &gen_compile_commands(&target_for_comp_cmds, &config.build_dir),
) )
.expect("fatal: could not write {file}"); .unwrap_or_else(|e| panic!("fatal: could not write {file}: {e}"));
} }
let build_dir = &config.build_dir; let build_dir = &config.build_dir;
@ -225,5 +226,6 @@ build {output_file}: regen_ninjafile {config_file_name} || $builddir/compile_com
ninjafile.push_str(&gen_ninja_code(&t)); ninjafile.push_str(&gen_ninja_code(&t));
} }
write(output_file, ninjafile).expect("fatal: could not write ninja file"); write(output_file, &ninjafile)
.unwrap_or_else(|e| panic!("could not write ninja file {ninjafile}: {e}"));
} }

View file

@ -46,14 +46,14 @@ impl Target {
if let Value::String(s) = table.get(k).unwrap() { if let Value::String(s) = table.get(k).unwrap() {
ret.outfile = s.to_owned(); ret.outfile = s.to_owned();
} else { } else {
panic!("fatal: {k} invalid type, must be a string."); panic!("error parsing target {name}: {k} invalid type, must be a string.");
} }
} }
"compiler" => { "compiler" => {
if let Value::String(s) = table.get(k).unwrap() { if let Value::String(s) = table.get(k).unwrap() {
ret.compiler = s.to_owned(); ret.compiler = s.to_owned();
} else { } else {
panic!("fatal: {k} invalid type, must be a string."); panic!("error parsing target {name}: {k} invalid type, must be a string.");
} }
} }
"compiler_flags" => { "compiler_flags" => {
@ -62,11 +62,11 @@ impl Target {
if let Value::String(s) = element { if let Value::String(s) = element {
ret.compiler_flags.push(s.to_owned()); ret.compiler_flags.push(s.to_owned());
} else { } else {
panic!("fatal: {k} invalid type, must be a array of strings."); panic!("error parsing target {name}: {k} invalid type, must be a array of strings.");
} }
} }
} else { } else {
panic!("fatal: {k} invalid type, must be a array of strings."); panic!("error parsing target {name}: {k} invalid type, must be a array of strings.");
} }
} }
"linker" => { "linker" => {
@ -74,7 +74,7 @@ impl Target {
ret.linker = s.to_owned(); ret.linker = s.to_owned();
linker_set = true; linker_set = true;
} else { } else {
panic!("fatal: {k} invalid type, must be a string."); panic!("error parsing target {name}: {k} invalid type, must be a string.");
} }
} }
"linker_flags" => { "linker_flags" => {
@ -83,11 +83,11 @@ impl Target {
if let Value::String(s) = element { if let Value::String(s) = element {
ret.linker_flags.push(s.to_owned()); ret.linker_flags.push(s.to_owned());
} else { } else {
panic!("fatal: {k} invalid type, must be a array of strings."); panic!("error parsing target {name}: {k} invalid type, must be a array of strings.");
} }
} }
} else { } else {
panic!("fatal: {k} invalid type, must be a array of strings."); panic!("error parsing target {name}: {k} invalid type, must be a array of strings.");
} }
} }
"linker_libs" => { "linker_libs" => {
@ -96,11 +96,11 @@ impl Target {
if let Value::String(s) = element { if let Value::String(s) = element {
ret.linker_libs.push(s.to_owned()); ret.linker_libs.push(s.to_owned());
} else { } else {
panic!("fatal: {k} invalid type, must be a array of strings."); panic!("error parsing target {name}: {k} invalid type, must be a array of strings.");
} }
} }
} else { } else {
panic!("fatal: {k} invalid type, must be a array of strings."); panic!("error parsing target {name}: {k} invalid type, must be a array of strings.");
} }
} }
"sources" => { "sources" => {
@ -109,24 +109,24 @@ impl Target {
if let Value::String(s) = element { if let Value::String(s) = element {
ret.sources.push(s.to_owned()); ret.sources.push(s.to_owned());
} else { } else {
panic!("fatal: {k} invalid type, must be a array of strings."); panic!("error parsing target {name}: {k} invalid type, must be a array of strings.");
} }
} }
} else { } else {
panic!("fatal: {k} invalid type, must be a array of strings."); panic!("error parsing target {name}: {k} invalid type, must be a array of strings.");
} }
} }
"default" => { "default" => {
if let Value::Boolean(b) = table.get(k).unwrap() { if let Value::Boolean(b) = table.get(k).unwrap() {
ret.is_default = *b; ret.is_default = *b;
} else { } else {
panic!("fatal: {k} invalid type, must be a boolean."); panic!("error parsing target {name}: {k} invalid type, must be a boolean.");
} }
} }
_ => { _ => {
if let Value::Table(_) = table.get(k).unwrap() { if let Value::Table(_) = table.get(k).unwrap() {
} else { } else {
panic!("fatal: unrecognized key {k}."); panic!("error parsing target {name}: unrecognized key {k}.");
} }
} }
} }
@ -137,7 +137,7 @@ impl Target {
} }
if ret.sources == Vec::<String>::new() { if ret.sources == Vec::<String>::new() {
panic!("fatal: you MUST specify at least one source"); panic!("error parsing target {name}: you MUST specify at least one source");
} }
ret ret