Skip to content

james-w/pls

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

33 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

pls - A Task Runner

pls runs tasks; kind of like Make, but more modern.

Features

Runner

  • ✔️ Define commands to run

  • ✔️ One-shot and daemon (start/stop)

  • ✔️ Dependencies - before and while

  • ✔️ Types: exec, docker, shell, cargo, ...

  • ✔️ Templates to be able to define one target, and then run it multiple ways?

  • ✔️ Arguments to allow passing extra args from cli

  • ✔️ Defaults for those arguments

  • ✔️ Env variables

  • ✔️ Return values, can be used by dependants, e.g. container name, port

  • ✔️ Status for daemons

  • 🔜 Logs for daemons

  • ✔️ List targets

  • ✔️ Descriptions

  • 🔜 Groups/tags

  • ✔️ Define artifacts to build, that are only rebuilt if needed

  • ✔️ Timestamp comparisons on files

  • ✔️ Timestamp comparison of last runtime when no file to check

  • ✔️ A way to force the build to happen

Watcher

  • ▶️ Standard watcher functionality
  • ⏩ Daemon RPC
  • ⏩ Enable/disable certain triggers
  • ▶️ Ordering (same as dependencies?)

UI

  • ⏩ TUI
  • ⏩ Log streams
  • ⏩ Highlight failures
  • ⏩ Parsing next/prev failure etc.

CI Integration

  • ⏩ Some way to flatten out to run the same commands in CI?

Usage

Usage is driven by a file called pls.toml. Create this at the root of your project (next to your .git directory), add add it to version control.

Commands

The file describes a series of commands that you can run, like this:

[command.exec.hello]
command = "echo hello world"

This defines a command called hello, so we can run it with:

$ pls run hello
[command.exec.hello] Running echo hello world
hello world

You can define a number of commands that do different things, and with different arguments depending on your needs.

[command.exec.hello]
command = "echo hello world"

[command.exec.goodbye]
command = "echo goodbye"

Container commands

You can also specify commands that run inside containers using podman.

[command.container.hello]
image = 'docker.io/alpine:latest
command = echo hello

They can then be run in exactly the same way.

$ pls run hello
[command.exec.hello] Running container using docker.io/alpine:latest
hello

This allows you to rely on specific versions of tools, or other cases where using a container is preferable.

Arguments

A command can take arguments passed from the command line.

[command.exec.echo_something]
command = "echo {args}"
$ pls run echo_something hello
[command.exec.hello] Running echo hello
hello

If you don't specify {args} in the command then they will be appended to the command.

Dependencies with requires

You can specify that one command needs to run after another by using the requires configuration option.

[command.container.one]
image = "docker.io/alpine:latest"
command = "echo one"

[command.exec.two]
command = "echo two"
requires = "one"

This will run the required command first.

$ pls run two
[command.container.one] Running with image docker.io/alpine:latest
[command.container.two] Running command "echo two"
two

Reuse with extends

There are many cases where you want to have similar commands with slight differences. This is supported with the extends option:

[command.exec.cargo]
command = "cargo"

[command.exec.test]
extends = "cargo"
args = "test"
$ pls run test
[command.exec.test] Running command "cargo test"
...

Variables

There are times when you want to avoid repeating something in the configuration, for instance the path to a file. For that there are variables that are substituted into commands and more.

[command.exec.write_config]
command = "./write_base_config {config_file}"
variables = { "config_file" = "base_config.yaml" }
$ pls run write_config
[command.exec.write_config] Running command "./write_base_config base_config.yaml"
...

You can then also refer to these variables from other targets:

[command.exec.show_config]
command = "cat {write_config.config_file}"

You can also specify global variables for things that don't belong to a single target:

[globals]
project_name = "foo"

[command.exec.show_project_name]
command = "echo {globals.project_name}"

Long-running commands with daemons

Sometimes the commands that you want to run are long-running, and are run in the background while doing other things, for instance dev servers. You define those commands as normal, but set the daemon option:

[command.exec.dev]
command = "npm run dev"
daemon = true

You can choose to run these commands as normal, and they will run in the foreground:

$ pls run dev
[command.exec.dev] Running command "npm run dev"
...
^C

However, when a command is defined as a daemon then you can also use the start and stop commands to run it in the backgound:

$ pls start dev
[command.exec.dev] Starting ...
$ pls stop dev
[command.exec.dev] Stopping ...

In addition, when one command requires a command that is defined as a daemon then it will be started as a pre-requisite, and then stopped after.

$ pls run integration_tests
[command.exec.db] Starting ...
[command.exec.integration_tests] Running command npm test
# ...
[command.exec.db] Stopping ...

Outputs

Certain commands produce outputs. These are similar to variables, but are defined at runtime depending on what the command does. Currently the only supported outputs are for containers:

Output Description
name The name of the container
network The name of the network if one was requested
[command.container.db]
image = "postgres"
create_network = true

[command.container.integration_tests]
image = "myimage"
network = "{db.outputs.network}"

This will allow the second container to run in the same network as the first while still allowing that network to be dynamic.

Artifacts

One of the most useful features of make is to avoid re-running commands if there's no need to. pls supports a similar concept based around "artifacts." These are very similar to commands, but produce a defined output. This allows for checks to be done to avoid the commands being re-run. There are some limitations though, for instance args aren't supported, as they would change the artifact that was produced, and so invalidate the comparisons.

Artifacts are built with the build command, or can also be ran with the run command to force it to be rebuilt.

Exec artifacts

There is an exec artifact type, much like the exec command type.

[artifact.exec.build]
command = "./build"
$ pls build
[artifact.exec.build] Building with command ./build
# ...
$ pls build
[artifact.exec.build] Up to date

Container Images

Another artifact type is a container image. This allows for a container image to be built using podman.

[artifact.container_image.foo]
context = "./container"
tag = "myimage"

Timestamp comparisons

Timestamp comparisons are similar to make. A target defines if_files_changed as an array of paths that should force a rebuild if they have changed.

[artifact.exec.build]
...
if_files_changed = ["src/*"]

A target can also define updates_paths, which are the paths that the command updates. If these are defined then they will be used in the comparison to see if the artifact should be rebuilt. If updates_paths is not defined then the last run time of the artifact will be compared with the files in if_files_changed.

Last-run comparisons

Sometimes there aren't files that can be tracked, or it's a lot of effort to do so. In that case you can fall back to comparison of the last-run time of targets. If you define an artifact without if_files_changed then the timestamps of the dependencies specified in requires will be used to decide whether to rebuild instead.

Forcing a rebuild to happen

Sometimes you want to rebuild an artifact, even if the dependencies haven't changed. In those cases just use the run command to build the artifact. Note that this only forces a rebuild of the specified target, any artifacts in the dependency chain will still be checked to see if they should be rebuilt.

Descriptions

Each target can have a description provided. This can help with remembering the purpose of a target, or to provide more information about how to use it. This is supported by all target types.

[artifact.container_image.base_image]
...
description = "Build the base image used for all other images in the project"

Watch Mode

When in a core development loop it's useful to have a "watch" running that triggers actions as needed based on the changes that you are making. This avoids you having to remember which commands to run in each case, and can also provider faster feedback to keep you in the flow state.

There are many commands available that offer this functionality, but pls can provide that functionality without having to repeat dependencies and mappings of which files should cause each target to be re-run.

To use this run

$ pls watch target1

This will run in the foreground and trigger targets to run as files change.

In order for this to work well you need to set up the if_files_changed and requires dependencies for your project. The benefit is that by getting these right once you get the watch functionality and much more, and it works for any target.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages