diff --git a/examples/build_system/.gitignore b/examples/build_system/.gitignore new file mode 100644 index 00000000000000..a25b681b2ed654 --- /dev/null +++ b/examples/build_system/.gitignore @@ -0,0 +1,6 @@ +# Files and directories made by build.vsh: +/target/ +/test.txt + +# Pre-compiled build.vsh +/build diff --git a/examples/build_system/build.vsh b/examples/build_system/build.vsh new file mode 100755 index 00000000000000..fa1e76e2fba183 --- /dev/null +++ b/examples/build_system/build.vsh @@ -0,0 +1,58 @@ +#!/usr/bin/env -S v run + +import build +import time + +// Define variables that can be used to change tasks in the buildscript +const app_name = 'hello' +const program_args = 'World' +const build_dir = 'target' + +// Make the build context +mut context := build.context( + // Set the default task to `release` when no arguments are provided + default: 'release' +) + +// Add a few simple tasks +context.task(name: 'doc', run: |self| system('echo "Nothing to do"')) +context.task(name: 'run', run: |self| system('v run . ${program_args}')) +context.task(name: 'build', run: |self| system('v .')) +context.task(name: 'build.prod', run: |self| system('v -prod -o ${app_name} .')) + +// `_` to denote "private" tasks. Nothing stops the user from using it, but +// this tells them that the task is not meant to be used by them. +context.task( + name: '_mkdirs' + // The `help` field is displayed in `--tasks` to give a short summary of what the task does. + help: 'Makes the directories used by the application' + run: fn (self build.Task) ! { + if !exists(build_dir) { + mkdir_all(build_dir) or { panic(err) } + } + } +) + +// This task will only run when the `test.txt` file is outdated +context.artifact( + name: 'test.txt' + help: 'Generate test.txt' + run: fn (self build.Task) ! { + write_file('test.txt', time.now().str())! + } +) + +// Add a more complex task +context.task( + name: 'release' + help: 'Build the app in production mode, generates documentation, and releases the build on Git' + depends: ['_mkdirs', 'doc', 'test.txt'] + run: fn (self build.Task) ! { + system('v -prod -o ${build_dir}/${app_name} .') + // Pretend we are using Git to publish the built file as a release here. + } +) + +// Run the build context. This will iterate over os.args and each corresponding +// task, skipping any arguments that start with a hyphen (-) +context.run() diff --git a/examples/build_system/main.v b/examples/build_system/main.v new file mode 100644 index 00000000000000..b9ed760124269d --- /dev/null +++ b/examples/build_system/main.v @@ -0,0 +1,5 @@ +import os + +fn main() { + println('Hello, ${os.args[1]}!') +} diff --git a/vlib/build/README.md b/vlib/build/README.md new file mode 100644 index 00000000000000..7ffe7492cc1887 --- /dev/null +++ b/vlib/build/README.md @@ -0,0 +1,52 @@ +## Description + +`build` provides a small build system leveraging V(SH) for the buildscript. + +## Example + +> See also: [build_system example](https://github.com/vlang/v/tree/master/examples/build_system) + +```v +#!/usr/bin/env -S v run + +import build +// .vsh automatically imports `os`, so you don't need this typically +import os { system } + +const app_name = 'vlang' +const program_args = 'World' + +mut context := build.context( + // Set the default task to `release` when no arguments are provided + default: 'release' +) + +context.task(name: 'doc', run: |self| system('v doc .')) +context.task(name: 'run', run: |self| system('v run . ${program_args}')) +context.task(name: 'build', run: |self| system('v .')) +context.task(name: 'build.prod', run: |self| system('v -prod .')) + +context.task( + name: 'release' + depends: ['doc'] + run: fn (self build.Task) ! { + system('v -prod -o build/${app_name} .') + // You could use Git to publish a release here too + } +) + +context.run() +``` + +## Pre-Compiling + +Running VSH scripts requires V to compile the script before executing it, which can cause a delay +between when you run `./build.vsh` and when the script actually starts executing. + +If you want to fix this, you can "pre-compile" the buildscript by building the script, i.e, running +`v -skip-running build.vsh`. + +> You will need to rebuild every time you change the buildscript, and you should also add `/build` +> to your `.gitignore` + +> If you want maximum speed, you can also `v -prod -skip-running build.vsh` \ No newline at end of file diff --git a/vlib/build/build.v b/vlib/build/build.v new file mode 100644 index 00000000000000..f0aa4996e07d09 --- /dev/null +++ b/vlib/build/build.v @@ -0,0 +1,180 @@ +module build + +import os + +@[heap; noinit] +pub struct BuildContext { +mut: + // should_run caches the result of should_run from tasks. + should_run map[string]bool + tasks []Task +pub mut: + // default is the default task to run when no others are provided. + default ?string +} + +@[heap; noinit] +pub struct Task { + run fn (Task) ! @[required] + should_run fn (Task) !bool @[required] + // repeatable controls whether or not this task can run multiple times per build cycle + repeatable bool +pub: + name string + help string + depends []string +mut: + did_run bool +} + +@[params] +pub struct BuildContextParams { +pub: + default ?string +} + +@[params] +pub struct TaskParams { +pub: + name string @[required] + help string + depends []string + should_run fn (Task) !bool = |self| true + run fn (Task) ! @[required] + // repeatable controls whether or not this task can run multiple times per build cycle + repeatable bool +} + +@[params] +pub struct ArtifactParams { +pub: + name string @[required] + help string + depends []string + should_run fn (Task) !bool = |self| !os.exists(self.name) + run fn (Task) ! @[required] + // repeatable controls whether or not this task can run multiple times per build cycle + repeatable bool +} + +// context creates an empty BuildContext. +pub fn context(params BuildContextParams) BuildContext { + return BuildContext{ + default: params.default + } +} + +// task creates a task for the given context. +pub fn (mut context BuildContext) task(config TaskParams) { + if context.get_task(config.name) != none { + eprintln('error: task already exists with name `${config.name}`') + exit(1) + } + context.tasks << Task{ + should_run: config.should_run + run: config.run + name: config.name + help: config.help + depends: config.depends + } +} + +// artifact creates an artifact task for the given context. +pub fn (mut context BuildContext) artifact(config ArtifactParams) { + if context.get_task(config.name) != none { + eprintln('error: task already exists with name `${config.name}`') + exit(1) + } + context.tasks << Task{ + should_run: config.should_run + run: config.run + name: config.name + help: config.help + depends: config.depends + repeatable: config.repeatable + } +} + +// get_task gets the task with the given name. +pub fn (mut context BuildContext) get_task(name string) ?&Task { + for mut task in context.tasks { + if task.name == name { + return mut task + } + } + return none +} + +// exec executes the task with the given name in the context. +pub fn (mut context BuildContext) exec(name string) { + if mut task := context.get_task(name) { + task.exec(mut context) + } else { + eprintln('error: no such task: ${name}') + exit(1) + } +} + +// exec runs the given task and its dependencies +pub fn (mut task Task) exec(mut context BuildContext) { + if task.did_run && !task.repeatable { + println(': ${task.name} (skipped)') + return + } + + if task.name !in context.should_run { + context.should_run[task.name] = task.should_run(task) or { + eprintln('error: failed to call should_run for task `${task.name}`: ${err}') + exit(1) + } + } + + if !context.should_run[task.name] { + println(': ${task.name} (skipped)') + return + } + + for dep in task.depends { + if dep == task.name { + eprintln('error: cyclic task dependency detected, `${task.name}` depends on itself') + exit(1) + } + + context.exec(dep) + } + println(': ${task.name}') + task.did_run = true + task.run(task) or { + eprintln('error: failed to run task `${task.name}`: ${err}') + exit(1) + } +} + +// run executes all tasks provided through os.args. +pub fn (mut context BuildContext) run() { + // filter out options + mut tasks := os.args[1..].filter(|it| !it.starts_with('-')) + + // check options + if '--tasks' in os.args || '-tasks' in os.args { + println('Tasks:') + for _, task in context.tasks { + println('- ${task.name}: ${task.help}') + } + return + } + + if tasks.len == 0 { + if context.default != none { + tasks << context.default + } else { + eprintln('error: no task provided, run with `--tasks` for a list') + exit(1) + } + } + + // execute tasks + for arg in tasks { + context.exec(arg) + } +}