Skip to content

Commit e972860

Browse files
build: add build system (#23853)
1 parent 711470d commit e972860

File tree

5 files changed

+301
-0
lines changed

5 files changed

+301
-0
lines changed

examples/build_system/.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Files and directories made by build.vsh:
2+
/target/
3+
/test.txt
4+
5+
# Pre-compiled build.vsh
6+
/build

examples/build_system/build.vsh

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
#!/usr/bin/env -S v run
2+
3+
import build
4+
import time
5+
6+
// Define variables that can be used to change tasks in the buildscript
7+
const app_name = 'hello'
8+
const program_args = 'World'
9+
const build_dir = 'target'
10+
11+
// Make the build context
12+
mut context := build.context(
13+
// Set the default task to `release` when no arguments are provided
14+
default: 'release'
15+
)
16+
17+
// Add a few simple tasks
18+
context.task(name: 'doc', run: |self| system('echo "Nothing to do"'))
19+
context.task(name: 'run', run: |self| system('v run . ${program_args}'))
20+
context.task(name: 'build', run: |self| system('v .'))
21+
context.task(name: 'build.prod', run: |self| system('v -prod -o ${app_name} .'))
22+
23+
// `_` to denote "private" tasks. Nothing stops the user from using it, but
24+
// this tells them that the task is not meant to be used by them.
25+
context.task(
26+
name: '_mkdirs'
27+
// The `help` field is displayed in `--tasks` to give a short summary of what the task does.
28+
help: 'Makes the directories used by the application'
29+
run: fn (self build.Task) ! {
30+
if !exists(build_dir) {
31+
mkdir_all(build_dir) or { panic(err) }
32+
}
33+
}
34+
)
35+
36+
// This task will only run when the `test.txt` file is outdated
37+
context.artifact(
38+
name: 'test.txt'
39+
help: 'Generate test.txt'
40+
run: fn (self build.Task) ! {
41+
write_file('test.txt', time.now().str())!
42+
}
43+
)
44+
45+
// Add a more complex task
46+
context.task(
47+
name: 'release'
48+
help: 'Build the app in production mode, generates documentation, and releases the build on Git'
49+
depends: ['_mkdirs', 'doc', 'test.txt']
50+
run: fn (self build.Task) ! {
51+
system('v -prod -o ${build_dir}/${app_name} .')
52+
// Pretend we are using Git to publish the built file as a release here.
53+
}
54+
)
55+
56+
// Run the build context. This will iterate over os.args and each corresponding
57+
// task, skipping any arguments that start with a hyphen (-)
58+
context.run()

examples/build_system/main.v

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import os
2+
3+
fn main() {
4+
println('Hello, ${os.args[1]}!')
5+
}

vlib/build/README.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
## Description
2+
3+
`build` provides a small build system leveraging V(SH) for the buildscript.
4+
5+
## Example
6+
7+
> See also: [build_system example](https://github.com/vlang/v/tree/master/examples/build_system)
8+
9+
```v
10+
#!/usr/bin/env -S v run
11+
12+
import build
13+
// .vsh automatically imports `os`, so you don't need this typically
14+
import os { system }
15+
16+
const app_name = 'vlang'
17+
const program_args = 'World'
18+
19+
mut context := build.context(
20+
// Set the default task to `release` when no arguments are provided
21+
default: 'release'
22+
)
23+
24+
context.task(name: 'doc', run: |self| system('v doc .'))
25+
context.task(name: 'run', run: |self| system('v run . ${program_args}'))
26+
context.task(name: 'build', run: |self| system('v .'))
27+
context.task(name: 'build.prod', run: |self| system('v -prod .'))
28+
29+
context.task(
30+
name: 'release'
31+
depends: ['doc']
32+
run: fn (self build.Task) ! {
33+
system('v -prod -o build/${app_name} .')
34+
// You could use Git to publish a release here too
35+
}
36+
)
37+
38+
context.run()
39+
```
40+
41+
## Pre-Compiling
42+
43+
Running VSH scripts requires V to compile the script before executing it, which can cause a delay
44+
between when you run `./build.vsh` and when the script actually starts executing.
45+
46+
If you want to fix this, you can "pre-compile" the buildscript by building the script, i.e, running
47+
`v -skip-running build.vsh`.
48+
49+
> You will need to rebuild every time you change the buildscript, and you should also add `/build`
50+
> to your `.gitignore`
51+
52+
> If you want maximum speed, you can also `v -prod -skip-running build.vsh`

vlib/build/build.v

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
module build
2+
3+
import os
4+
5+
@[heap; noinit]
6+
pub struct BuildContext {
7+
mut:
8+
// should_run caches the result of should_run from tasks.
9+
should_run map[string]bool
10+
tasks []Task
11+
pub mut:
12+
// default is the default task to run when no others are provided.
13+
default ?string
14+
}
15+
16+
@[heap; noinit]
17+
pub struct Task {
18+
run fn (Task) ! @[required]
19+
should_run fn (Task) !bool @[required]
20+
// repeatable controls whether or not this task can run multiple times per build cycle
21+
repeatable bool
22+
pub:
23+
name string
24+
help string
25+
depends []string
26+
mut:
27+
did_run bool
28+
}
29+
30+
@[params]
31+
pub struct BuildContextParams {
32+
pub:
33+
default ?string
34+
}
35+
36+
@[params]
37+
pub struct TaskParams {
38+
pub:
39+
name string @[required]
40+
help string
41+
depends []string
42+
should_run fn (Task) !bool = |self| true
43+
run fn (Task) ! @[required]
44+
// repeatable controls whether or not this task can run multiple times per build cycle
45+
repeatable bool
46+
}
47+
48+
@[params]
49+
pub struct ArtifactParams {
50+
pub:
51+
name string @[required]
52+
help string
53+
depends []string
54+
should_run fn (Task) !bool = |self| !os.exists(self.name)
55+
run fn (Task) ! @[required]
56+
// repeatable controls whether or not this task can run multiple times per build cycle
57+
repeatable bool
58+
}
59+
60+
// context creates an empty BuildContext.
61+
pub fn context(params BuildContextParams) BuildContext {
62+
return BuildContext{
63+
default: params.default
64+
}
65+
}
66+
67+
// task creates a task for the given context.
68+
pub fn (mut context BuildContext) task(config TaskParams) {
69+
if context.get_task(config.name) != none {
70+
eprintln('error: task already exists with name `${config.name}`')
71+
exit(1)
72+
}
73+
context.tasks << Task{
74+
should_run: config.should_run
75+
run: config.run
76+
name: config.name
77+
help: config.help
78+
depends: config.depends
79+
}
80+
}
81+
82+
// artifact creates an artifact task for the given context.
83+
pub fn (mut context BuildContext) artifact(config ArtifactParams) {
84+
if context.get_task(config.name) != none {
85+
eprintln('error: task already exists with name `${config.name}`')
86+
exit(1)
87+
}
88+
context.tasks << Task{
89+
should_run: config.should_run
90+
run: config.run
91+
name: config.name
92+
help: config.help
93+
depends: config.depends
94+
repeatable: config.repeatable
95+
}
96+
}
97+
98+
// get_task gets the task with the given name.
99+
pub fn (mut context BuildContext) get_task(name string) ?&Task {
100+
for mut task in context.tasks {
101+
if task.name == name {
102+
return mut task
103+
}
104+
}
105+
return none
106+
}
107+
108+
// exec executes the task with the given name in the context.
109+
pub fn (mut context BuildContext) exec(name string) {
110+
if mut task := context.get_task(name) {
111+
task.exec(mut context)
112+
} else {
113+
eprintln('error: no such task: ${name}')
114+
exit(1)
115+
}
116+
}
117+
118+
// exec runs the given task and its dependencies
119+
pub fn (mut task Task) exec(mut context BuildContext) {
120+
if task.did_run && !task.repeatable {
121+
println(': ${task.name} (skipped)')
122+
return
123+
}
124+
125+
if task.name !in context.should_run {
126+
context.should_run[task.name] = task.should_run(task) or {
127+
eprintln('error: failed to call should_run for task `${task.name}`: ${err}')
128+
exit(1)
129+
}
130+
}
131+
132+
if !context.should_run[task.name] {
133+
println(': ${task.name} (skipped)')
134+
return
135+
}
136+
137+
for dep in task.depends {
138+
if dep == task.name {
139+
eprintln('error: cyclic task dependency detected, `${task.name}` depends on itself')
140+
exit(1)
141+
}
142+
143+
context.exec(dep)
144+
}
145+
println(': ${task.name}')
146+
task.did_run = true
147+
task.run(task) or {
148+
eprintln('error: failed to run task `${task.name}`: ${err}')
149+
exit(1)
150+
}
151+
}
152+
153+
// run executes all tasks provided through os.args.
154+
pub fn (mut context BuildContext) run() {
155+
// filter out options
156+
mut tasks := os.args[1..].filter(|it| !it.starts_with('-'))
157+
158+
// check options
159+
if '--tasks' in os.args || '-tasks' in os.args {
160+
println('Tasks:')
161+
for _, task in context.tasks {
162+
println('- ${task.name}: ${task.help}')
163+
}
164+
return
165+
}
166+
167+
if tasks.len == 0 {
168+
if context.default != none {
169+
tasks << context.default
170+
} else {
171+
eprintln('error: no task provided, run with `--tasks` for a list')
172+
exit(1)
173+
}
174+
}
175+
176+
// execute tasks
177+
for arg in tasks {
178+
context.exec(arg)
179+
}
180+
}

0 commit comments

Comments
 (0)