Add additional content around README.

pull/9/head
Joshua Potter 2023-11-25 15:25:01 -07:00
parent 27c097eb72
commit eb598fe639
14 changed files with 156 additions and 59 deletions

2
.gitignore vendored
View File

@ -3,4 +3,4 @@
bootstrap bootstrap
compile_commands.json compile_commands.json
docs/ docs/
test/runner test/test

View File

@ -8,10 +8,10 @@ build: include/*.h src/*.c
bear: include/*.h src/*.c bear: include/*.h src/*.c
@bear -- ${BUILD} @bear -- ${BUILD}
test: test/runner test: test/test
$^ $^
test/runner: include/*.h src/*.c test/*.h test/*.c test/test: include/*.h src/*.c test/*.h test/*.c
clang -I include src/*.c test/*.c -o test/runner -lm clang -I include src/*.c test/*.c -o test/test -lm
.PHONY: test .PHONY: test

136
README.md
View File

@ -5,6 +5,9 @@ CLI utility for defining custom project initialization scripts.
TODO: TODO:
- [ ] Add evaluator tests. - [ ] Add evaluator tests.
- [ ] Color output to console. - [ ] Color output to console.
- [ ] string -> line, case insensitive
- [ ] Ensure keys are alphanumeric, underscore
- [ ] sophie
## Overview ## Overview
@ -21,17 +24,19 @@ this project will feel at home. Ultimately the goal is to create (optionally)
interactive scripts like those mentioned in the above list to quickly scaffold interactive scripts like those mentioned in the above list to quickly scaffold
your new projects in a consistent way. your new projects in a consistent way.
---
We start with an example. Consider the following *spec*, which we'll name We start with an example. Consider the following *spec*, which we'll name
`example`: `touch` (this example exists as a [pre-packaged spec](./specs/touch)):
```json ```json
{ {
"filename": { "filename": {
"type": "STRING", "type": "text",
"prompt": "What file should I create for you? " "prompt": "What file should I create for you? "
} }
} }
``` ```
and its associated *builder*: and its associated *runner*:
```bash ```bash
#!/usr/bin/env bash #!/usr/bin/env bash
@ -42,40 +47,131 @@ touch "$OUT/$FILENAME"
Running `bootstrap` with these two files configured will invoke the following Running `bootstrap` with these two files configured will invoke the following
interactive script: interactive script:
```bash ```bash
> bootstrap example $> bootstrap touch
I: What file should I create for you? hello-world.txt What file should I create for you? hello-world.txt
O: Creating hello-world.txt Creating hello-world.txt
$> ls
> ... hello-world.txt ...
``` ```
Here the line prefixed with `I:` indicates a prompt that must be answered by You should now see a new `hello-world.txt` file in your current working
the user. The line prefixed with `O:` indicates the output of the builder
script. You should now see a new `hello-world.txt` file in your current working
directory. directory.
## Usage ## Usage
TODO
### Installation ### Installation
TODO TODO
### Specs and Builders ### Runners
TODO A spec refers to any directory containing a file named `runner`. The only
requirement enforced by `bootstrap` is for this file to be an executable (e.g.
`chmod +x`), but typically the `runner` is a shell script:
```bash
#!/usr/bin/env bash
...
```
The `runner` is invoked with its current working directory set to that of the
directory containing it. For instance, if we have a `runner` script living in
directory `~/Documents/specs/example` with contents:
```bash
#!/usr/bin/env bash
echo "$PWD"
```
the output of `bootstrap example` will *always* be e.g.
```bash
> bootstrap example
/home/jrpotter/Documents/specs/example
```
regardless of where we call the `bootstrap` command.
#### Exit Code
`bootstrap` always invokes the `runner` using the system `sh` command:
```bash
> /bin/sh sh -c ./runner
```
The exit code emitted by `bootstrap` will mirror that returned by the `runner`
executable.
### Specs
If interested in making the `runner` more flexible, you can provide different
environment variables in the form of a `spec.json` file. This file must live
in the same spec as the `runner`. When invoking `bootstrap`, the file is used to
determine what prompts should be displayed to the user before executing the
`runner` file. The user's responses are then included as environment variables
to the `runner` process.
The file contents should consist of a top-level JSON object and any number of
child objects called *fields*. A typical `spec.json` file looks like:
```json
{
"fieldname": {
"type": "text",
"prompt": "Prompt for field> "
},
...
}
```
In this example, the first field is called `"fieldname"`. `bootstrap` sees this
field and writes the prompt `"Prompt for field> "` to `stdout`. Since
`"fieldname"` has type `"text"`, `bootstrap` will wait for the user to input
a string (submitted with a newline).
If the user were to enter, say `fieldvalue`, in response to the prompt,
the `runner` script would then have access to an environment variable
`FIELDNAME` set to `fieldvalue` on launch.
#### Types
The value of `type` determines how a field is prompted for. Note the value of
`type` is case insenstive. The currently supported list of types are:
* `text`
* The simplest prompt type. Takes in a free-form response submitted after a
newline (`\n`) is encountered.
#### Root Directory
All specs should exist in the same *root directory*. As an example of what this
directory might look like, refer to `specs` at the top-level of this project.
When invoking `bootstrap <name>`, `<name>` is expected to correspond to some
spec found within the root directory.
To tell `bootstrap` where your specs are located, you can provide the path to
the root directory using the `-d` option like so:
```bash
> bootstrap -d ~/Documents/specs example
...
```
If no option is set, `bootstrap` will fallback to using the value of the
`BOOTSTRAP_ROOT_DIR` environment variable. If this also isn't set, `bootstrap`
will abort with an appropriate error message.
### Other Environment Variables ### Other Environment Variables
TODO By default, the `runner` command will have the following environment variables
defined. Defining these fields in a `spec.json` file will override the default
values:
* `OUT`
* The directory `bootstrap` was invoked from. Named since this is usually
where you want to initialize new files of your project in.
### Supplied Specs ### Supplied Specs
TODO A number of specs are provided out of the box. If you installed `bootstrap`
using `nix`, the `BOOTSTRAP_ROOT_DIR` will automatically be set to the location
of these [specs](./specs). Keep in mind this list is very opinionated - they
reflect my personal needs for projects. Feel free to specify a different specs
root directory if these do not fit your needs.
### Using With Nix As a suggestion, use `nix` from within your `runner` scripts for maximum
reproducibility. Refer to the provided specs for inspiration on how you can do
TODO this.
## Development ## Development

View File

@ -45,10 +45,10 @@ enum ErrorCode {
/// The `prompt` of a `spec.json` field is not a string. /// The `prompt` of a `spec.json` field is not a string.
ERROR_VALIDATOR_FIELD_PROMPT_INVALID, ERROR_VALIDATOR_FIELD_PROMPT_INVALID,
/// The `run.sh` file could not be found. /// The `runner` file could not be found.
ERROR_EVALUATOR_RUN_SH_NOT_FOUND, ERROR_EVALUATOR_RUNNER_NOT_FOUND,
/// The `run.sh` file is not executable. /// The `runner` file is not executable.
ERROR_EVALUATOR_RUN_SH_NOT_EXEC, ERROR_EVALUATOR_RUNNER_NOT_EXEC,
/// A user response to a prompt is not valid. /// A user response to a prompt is not valid.
ERROR_EVALUATOR_RESPONSE_INVALID, ERROR_EVALUATOR_RESPONSE_INVALID,
}; };

View File

@ -1,6 +1,6 @@
/** /**
@file @file
@brief `spec.json` and `run.sh` evaluator. @brief `spec.json` and `runner` evaluator.
*/ */
#ifndef _BOOTSTRAP_EVALUATOR_H #ifndef _BOOTSTRAP_EVALUATOR_H
#define _BOOTSTRAP_EVALUATOR_H #define _BOOTSTRAP_EVALUATOR_H
@ -10,21 +10,21 @@
#include "validator.h" #include "validator.h"
/** /**
@brief Run the `run.sh` script found in the configured spec. @brief Run the `runner` file found in the configured spec.
@param config @param config
A reference to the parameters describing the desired spec. A reference to the parameters describing the desired spec.
@param fields @param fields
The list of prompts to have answered by the user prior to executing `run.sh`. The list of prompts to have answered by the user prior to executing `runner`.
Responses are included as environment variables in the invoked subshell. Responses are included as environment variables in the invoked subshell.
@param error @param error
The out parameter containing a possible error. Set to a null pointer if no The out parameter containing a possible error. Set to a null pointer if no
error occurs. Otherwise set to an @ref Error instance. error occurs. Otherwise set to an @ref Error instance.
@return @return
If @p error is set, returns `EXIT_FAILURE`. Otherwise returns the exit code If @p error is set, returns `EXIT_FAILURE`. Otherwise returns the exit code
returned by `run.sh`. returned by the `runner`.
*/ */
int evaluate_run_sh( int evaluate_runner(
const struct Config *const config, const struct Config *const config,
const struct DynArray *const fields, const struct DynArray *const fields,
struct Error **error struct Error **error

View File

@ -13,7 +13,7 @@
@brief The types of fields `bootstrap` can handle. @brief The types of fields `bootstrap` can handle.
*/ */
enum FieldType { enum FieldType {
FT_STRING = 1, FT_TEXT = 1,
}; };
/** /**
@ -25,12 +25,12 @@ file. For instance, the fields of:
```json ```json
{ {
"abc": { "abc": {
"type": "STRING", "type": "text",
"prompt": "ABC" "prompt": "ABC> "
}, },
"def": { "def": {
"type": "STRING", "type": "text",
"prompt": "DEF" "prompt": "DEF> "
}, },
} }
``` ```

2
main.c
View File

@ -38,7 +38,7 @@ static int run(const char *root_dir, const char *target) {
goto cleanup_parsed; goto cleanup_parsed;
} }
if ((retval = evaluate_run_sh(config, prompts, &error))) { if ((retval = evaluate_runner(config, prompts, &error))) {
if (error) { if (error) {
fprintf(stderr, "%s", error->message); fprintf(stderr, "%s", error->message);
} }

View File

@ -1,3 +0,0 @@
#!/usr/bin/env bash
echo "hello world"

View File

@ -1,6 +0,0 @@
{
"display": {
"type": "STRING",
"prompt": "This is a prompt"
}
}

4
specs/touch/runner Executable file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env bash
echo "Creating $FILENAME"
touch "$OUT/$FILENAME"

6
specs/touch/spec.json Normal file
View File

@ -0,0 +1,6 @@
{
"filename": {
"type": "text",
"prompt": "What file should I create for you? "
}
}

View File

@ -11,11 +11,11 @@
#include "string_buf.h" #include "string_buf.h"
#include "validator.h" #include "validator.h"
static struct Error *find_run_sh(const struct Config *const config) { static struct Error *find_run_exec(const struct Config *const config) {
assert(config); assert(config);
struct stat sb; struct stat sb;
const char *segments[] = {config->root_dir, config->target, "run.sh"}; const char *segments[] = {config->root_dir, config->target, "runner"};
char *filepath = char *filepath =
join_path_segments(sizeof(segments) / sizeof(char *), segments); join_path_segments(sizeof(segments) / sizeof(char *), segments);
int stat_res = stat(filepath, &sb); int stat_res = stat(filepath, &sb);
@ -23,18 +23,18 @@ static struct Error *find_run_sh(const struct Config *const config) {
if (stat_res == -1 && errno == ENOENT) { if (stat_res == -1 && errno == ENOENT) {
return ERROR_NEW( return ERROR_NEW(
ERROR_EVALUATOR_RUN_SH_NOT_FOUND, ERROR_EVALUATOR_RUNNER_NOT_FOUND,
"Could not find ", "Could not find ",
config->target, config->target,
"/run.sh" "/runner"
); );
} }
if (!(sb.st_mode & S_IXUSR)) { if (!(sb.st_mode & S_IXUSR)) {
return ERROR_NEW( return ERROR_NEW(
ERROR_EVALUATOR_RUN_SH_NOT_EXEC, ERROR_EVALUATOR_RUNNER_NOT_EXEC,
config->target, config->target,
"/run.sh is not executable." "/runner is not executable."
); );
} }
@ -45,7 +45,7 @@ static const char *prompt_field(struct Field *field) {
assert(field); assert(field);
switch (field->type) { switch (field->type) {
case FT_STRING: case FT_TEXT:
printf("%s", field->prompt); printf("%s", field->prompt);
// TODO: Probably want this buffer size to be a bit more dynamic. // TODO: Probably want this buffer size to be a bit more dynamic.
char *input = calloc(1, 1024); char *input = calloc(1, 1024);
@ -74,12 +74,12 @@ static void push_env(
string_buf_sappend(env, "' "); string_buf_sappend(env, "' ");
} }
int evaluate_run_sh( int evaluate_runner(
const struct Config *const config, const struct Config *const config,
const struct DynArray *const fields, const struct DynArray *const fields,
struct Error **error struct Error **error
) { ) {
*error = find_run_sh(config); *error = find_run_exec(config);
if (*error) { if (*error) {
return EXIT_FAILURE; return EXIT_FAILURE;
} }
@ -102,7 +102,7 @@ int evaluate_run_sh(
} }
} }
const char *segments[] = {config->root_dir, config->target, "run.sh"}; const char *segments[] = {config->root_dir, config->target, "runner"};
const char *filepath = const char *filepath =
join_path_segments(sizeof(segments) / sizeof(char *), segments); join_path_segments(sizeof(segments) / sizeof(char *), segments);
const char *env = string_buf_convert(env_buf); const char *env = string_buf_convert(env_buf);

View File

@ -28,7 +28,7 @@ static struct Error *read_field(const cJSON *const field, struct Field **out) {
} }
if (strcmp(type->valuestring, "STRING") == 0) { if (strcmp(type->valuestring, "STRING") == 0) {
(*out)->type = FT_STRING; (*out)->type = FT_TEXT;
} else { } else {
error = ERROR_NEW( error = ERROR_NEW(
ERROR_VALIDATOR_FIELD_TYPE_UNKNOWN, ERROR_VALIDATOR_FIELD_TYPE_UNKNOWN,