Rework build scripts (close #5)

- Separate cross-compilation logic to crosscompile.vsh
- Manage Docker in make.vsh, no more manual running `docker build`, etc.
- Add MANUAL.md instruction
- Enchance Dockerfile
This commit is contained in:
ge
2025-11-18 01:35:57 +03:00
parent b81f6a5f29
commit 2c65ab66a9
5 changed files with 435 additions and 332 deletions

View File

@@ -10,8 +10,7 @@ RUN apt-get update && \
apt-get clean && rm -rf /var/cache/apt/archives/* && rm -rf /var/lib/apt/lists/* apt-get clean && rm -rf /var/cache/apt/archives/* && rm -rf /var/lib/apt/lists/*
RUN git clone --depth=1 https://github.com/vlang/v /opt/v && \ RUN git clone --depth=1 https://github.com/vlang/v /opt/v && \
cd /opt/v && \ make -C /opt/v && \
make && \
/opt/v/v symlink && \ /opt/v/v symlink && \
v version v version
@@ -36,4 +35,5 @@ WORKDIR /app
USER 1000:1000 USER 1000:1000
ENV VMODULES=/tmp/.vmodules ENV VMODULES=/tmp/vmodules
ENV VCACHE=/tmp/vcache

138
MANUAL.md Normal file
View File

@@ -0,0 +1,138 @@
# Manual cross-compilation
This repository contains ready-made scripts that allow you to describe the
desired results and obtain them simply by running `./make.vsh`. However, to
better understand the process, it's worth considering the manual build.
This file describes the algorithm of actions automated in the aforementioned
scripts.
## Prepare the environment
We want reproducible builds. We also don't want to clutter our computer with
things needed exclusively for cross-compilation. Besides manipulating certain
packages in the OS can inadvertently damage the system.
Docker will help us achieve all our goals.
[Install it](https://docs.docker.com/get-started/get-docker/) if you haven't
already. Containers will ensure reproducible builds, as they will always run
in the same environment.
## Let's begin
Create a V programm. Just initialize empty V project in some dir:
```console
$ mkdir crossv
$ cd crossv
$ v init
Input your project description: Cross-compilation example
Input your project version: (0.0.0)
Input your project license: (MIT)
Initialising ...
Created binary (application) project `crossv`
```
Contents of `main.v`:
```v
module main
fn main() {
println('Hello World!')
}
```
There is already an example Dockerfile in this repository, so here I will focus
on CLI. So let's run Debian Linux in container with current directory mounted:
```
docker run --rm -ti -v .:/app -w /app debian:trixie
```
See https://docs.docker.com/reference/cli/docker/container/run/ for details.
Now we will run shell commands inside container.
## Setup V compiler in container
Install prerequisistes:
```
apt update
apt install -y --no-install-recommends --no-install-suggests build-essential git ca-certificates file
```
Download and bootstrap V compiler:
```
export VMODULES=/tmp/vmodules VCACHE=/tmp/vcache
git clone --depth=1 https://github.com/vlang/v /opt/v && make -C /opt/v && /opt/v/v symlink
```
After this `v` command should work. Try:
```
v version
```
## Cross-compile to ARM64 (AArch64)
Your host is most likely an x86_64 computer. For the sake of example, let's
compile our Linux program for the AArch64 architecture.
First we need to add build requirements. Debian already have an excellent
[cross-compiling](//wiki.debian.org/CrossCompiling) support.
Prepate Debian package manager:
```
dpkg --add-architecture arm64
apt update
```
We need to install `crossbuild-essential-arm64` package:
```
apt install -y --no-install-recommends --no-install-suggests crossbuild-essential-arm64
```
Also there is packages for some other architectures which Debian supports:
https://packages.debian.org/search?keywords=crossbuild-essential&searchon=names&suite=stable&section=all
Now we have a GCC cross-compiler and some common libraries for ARM64.
To compile out project just run:
```
v -prod -cc aarch64-linux-gnu-gcc -o hello .
```
Let's make sure we've built the correct executable using the `file` utility:
```console
# file hello
hello: ELF 64-bit LSB pie executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, BuildID[sha1]=e9cfdee9abe5a80c304d489f243fbc60a22d93de, for GNU/Linux 3.7.0, not stripped
```
Binary is dynamically linked. To produce statically linked binary add `-cflags -static` flag:
```
v -prod -cc aarch64-linux-gnu-gcc -cflags -static -o hello .
```
Done.
Since we were operating as the root user inside the container, it's worth
changing the file owner:
```
chown 1000:1000 hello
```
Replace `1000:1000` with your actual `UID:GID` pair on host system.
Now we can exit from container:
```
exit
```

View File

@@ -14,7 +14,7 @@ Produced binaries:
* Linux: `amd64`, `arm64`, `arm32` (`armhf`), `ppc64le`, `s390x`, `riscv64` * Linux: `amd64`, `arm64`, `arm32` (`armhf`), `ppc64le`, `s390x`, `riscv64`
* Windows: `amd64` * Windows: `amd64`
* FreeBSD: `amd64` * ~~FreeBSD: `amd64`~~ (disabled for now)
The example programm is just `Hello World!`. For complex programs you may need The example programm is just `Hello World!`. For complex programs you may need
to add more dependencies in build container. to add more dependencies in build container.
@@ -23,60 +23,30 @@ I relied on Debian's excellent cross-compilation support (see the Dockerfile),
but with some elbow grease, you can compile the program for other architectures but with some elbow grease, you can compile the program for other architectures
and operating systems. and operating systems.
Build: **Build**
Run:
``` ```
docker build . -t vlang-cross:latest-trixie ./make.vsh
``` ```
make.vsh script will build the Docker image and run crosscompile.vsh inside
a container.
The container image is large (almost 3GiB) due to the number of libraries The container image is large (almost 3GiB) due to the number of libraries
required for cross-compilation. The size could actually be reduced, but that's required for cross-compilation. The size could actually be reduced, but that's
what Debian provides by default in the `crossbuild-essential-*` packages. For what Debian provides by default in the `crossbuild-essential-*` packages. For
the same reason, building the image isn't very fast (up to ~3 minutes for me). the same reason, building the image isn't very fast (up to ~3 minutes for me).
Start cross-compilation: You may need change `docker_command` in `make.vsh` to `sudo docker` if your
host user does not have access to Docker daemon.
``` Look inside `release/` dir after compilation (:
docker run --rm -v .:/app vlang-cross:latest-trixie env DEBUG=1 ./make.vsh
```
then look inside `release/` dir (:
## Synopsis
You can run the make.vsh script in two ways:
```
./make.vsh
# or
v run make.vsh
```
```
Build script options:
-tasks List available tasks.
-help Print this help message and exit. Aliases: help, --help.
Build can be configured throught environment variables:
BUILD_PROG_NAME Name of the compiled program. By default the name is
parsed from v.mod.
BUILD_PROG_VERSION Version of the compiled program. By default the name
is parsed from v.mod.
BUILD_PROG_ENTRYPOINT The program entrypoint. Defaults to '.' (current dir).
BUILD_OUTPUT_DIR The directory where the build artifacts will be placed.
Defaults to './release'.
BUILD_SKIP_TARGETS List of build targets to skip. Expects comma-separated
list without whitespaces e.g. 'windows-amd64,linux-armhf'
BUILD_COMMON_VFLAGS The list of V flags is common for all targets. Expects
comma-separated list. Default is '-prod,-cross'.
BUILD_COMMON_CFLAGS Same as BUILD_COMMON_VFLAGS, but passed to underlying
C compiler. Default is '-static'.
DEBUG If set enables the verbose output as dimmed text.
```
## See Also ## See Also
* [MANUAL.md](MANUAL.md) in this repository.
* `v help build` * `v help build`
* `v help build-c` * `v help build-c`
* https://docs.vlang.io/cross-compilation.html * https://docs.vlang.io/cross-compilation.html

234
crosscompile.vsh Executable file
View File

@@ -0,0 +1,234 @@
#!/usr/bin/env -S v run
import arrays.parallel
import os
import os.cmdline
import term
import v.vmod
/*
SETTING BUILD TARGETS
All build targets must be defined in the build_targets const below.
Each target is a V struct. Fields are:
name string
The target name. Prefer to use https://wiki.osdev.org/Target_Triplet
Note that target name will be used in output file name. For example
the 'linux-riscv64' becomes to 'myprog-1.2.3-linux-riscv64'. This is
very common naming scheme for compiled program distributions.
cc string
C Compiler to use e.g. '/usr/bin/gcc', 'clang', etc.
vflags []string
cflags []string
ldflags []string
Flags which will be passed to V compiler. See `v help build-c` for info.
filename string
Output file naming pattern. By default is '%n-%v-%t'.
%n will be replaced with the program name (from v.mod by default)
%v will be replaced with the program version (also from v.mod)
%t will be replaced with the target name from `name` field.
For example this is useful for Windows builds: for target named
'windows-amd64' and '%n-%v-%t.exe' filename pattern value you will get
artifact named 'progname-1.2.3-windows-amd64.exe'.
See also Target struct definition below.
V'S SPECIAL ENVIRONMENT VARIABLES
VCROSS_COMPILER_NAME See vcross_compiler_name() in v.pref module.
VCROSS_LINKER_NAME See vcross_linker_name() in v.pref module.
*/
const build_targets = [
Target{
name: 'linux-amd64'
cc: 'gcc'
},
Target{
name: 'linux-arm64'
cc: 'aarch64-linux-gnu-gcc'
},
Target{
name: 'linux-armhf'
cc: 'arm-linux-gnueabihf-gcc'
},
Target{
name: 'linux-ppc64le'
cc: 'powerpc64le-linux-gnu-gcc'
},
Target{
name: 'linux-s390x'
cc: 's390x-linux-gnu-gcc'
},
Target{
name: 'linux-riscv64'
cc: 'riscv64-linux-gnu-gcc'
},
Target{
name: 'windows-amd64'
vflags: ['-os', 'windows']
filename: '%n-%v-%t.exe'
},
// FreeBSD build is buggy, disable it for now...
// Target{
// name: 'freebsd-amd64'
// vflags: ['-os', 'freebsd']
// },
]
struct Target {
name string
cc string
vflags []string
cflags []string
ldflags []string
filename string = '%n-%v-%t'
}
fn (target Target) output_file() string {
// vfmt off
return target.filename.replace_each([
'%n', build_config.program_name,
'%v', build_config.program_version,
'%t', target.name,
])
// vfmt on
}
const build_config = BuildConfig.new()
struct BuildConfig {
program_name string
program_version string
program_entrypoint string
output_dir string
}
fn BuildConfig.new() BuildConfig {
manifest := vmod.decode(@VMOD_FILE) or { vmod.Manifest{} }
return BuildConfig{
program_name: os.getenv_opt('BUILD_PROG_NAME') or { manifest.name }
program_version: os.getenv_opt('BUILD_PROG_VERSION') or { manifest.version }
program_entrypoint: os.getenv_opt('BUILD_PROG_ENTRYPOINT') or { '.' }
output_dir: os.abs_path(os.norm_path(os.getenv_opt('BUILD_OUTPUT_DIR') or {
'release'
}))
}
}
fn make_build(build_target Target) ! {
artifact := os.join_path_single(build_config.output_dir, build_target.output_file())
eprintln(term.bold('Building artifact: ${artifact}'))
os.mkdir_all(os.dir(artifact)) or {}
mut vargs := []string{}
if build_target.cc != '' {
vargs << ['-cc', build_target.cc]
}
for vflag in build_target.vflags {
vargs << vflag
}
for cflag in build_target.cflags {
vargs << ['-cflags', cflag]
}
for ldflag in build_target.ldflags {
vargs << ['-ldflags', ldflag]
}
vargs << ['-o', artifact]
vargs << build_config.program_entrypoint
execute_command(@VEXE, vargs)!
}
fn execute_command(executable string, args []string) ! {
path := os.find_abs_path_of_executable(executable) or { os.norm_path(executable) }
printdbg("Run '${path}' with arguments: ${args}")
mut proc := os.new_process(path)
proc.set_args(args)
proc.set_work_folder(os.getwd())
proc.run()
proc.wait()
if proc.status == .exited && proc.code != 0 {
return error('Command ${term.bold(path)} exited with non-zero code ${proc.code}')
}
}
fn printdbg(s string) {
if os.getenv('DEBUG') !in ['', '0', 'false', 'no'] {
eprintln(term.dim(s))
}
}
@[noreturn]
fn errexit(s string) {
eprintln(term.failed('Error: ${s}'))
exit(1)
}
fn main() {
args := os.args[1..]
mut targets := map[string]Target{}
for target in build_targets {
targets[target.name] = target
}
options := cmdline.only_options(args)
if args.contains('help') || options.contains('-help') || options.contains('--help') {
println(help_text)
exit(0)
}
if options.contains('-targets') {
for name, _ in targets {
println(name)
}
exit(0)
}
if options.contains('-release') {
os.setenv('VFLAGS', '${os.getenv('VFLAGS')} -prod -cflags -static'.trim_space(),
true)
}
printdbg('Args: ${args}')
printdbg('VFLAGS=${os.getenv('VFLAGS')}')
printdbg('VJOBS=${os.getenv('VJOBS')}')
printdbg(build_config.str())
mut to_build := []Target{}
for arg in cmdline.only_non_options(args) {
to_build << targets[arg] or { errexit("Invalid target: '${arg}', abotring...") }
}
if to_build.len == 0 {
to_build = targets.values()
}
parallel.run(to_build, |build_target| make_build(build_target) or { errexit(err.msg()) })
}
const help_text = "
Build script options:
-targets List available targets.
-help Print this help message and exit. Aliases: help, --help.
-release Pass '-prod -cflags -static' flags to V.
Build can be configured throught environment variables:
DEBUG If set enables the verbose output as dimmed text.
BUILD_PROG_NAME Name of the compiled program. By default the name is
parsed from v.mod.
BUILD_PROG_VERSION Version of the compiled program. By default version
is parsed from v.mod.
BUILD_PROG_ENTRYPOINT The program entrypoint. Defaults to '.' (current dir).
Specify file or module which have fn main() defined.
BUILD_OUTPUT_DIR The directory where the build artifacts will be placed.
Defaults to './release'.
V-specific environment variables:
VFLAGS Set arbitrary flags for all jobs.
VJOBS Number of parallel jobs. Set it to enchanse compile speed.
".trim_indent()

335
make.vsh
View File

@@ -2,305 +2,66 @@
import build import build
import crypto.sha256 import crypto.sha256
import maps
import os import os
import term import term
import v.vmod
const program_name = os.getenv_opt('BUILD_PROG_NAME') or { vmod_name() } const output_dir = './release'
const program_version = os.getenv_opt('BUILD_PROG_VERSION') or { vmod_version() } const docker_command = 'docker'
const program_entrypoint = os.getenv_opt('BUILD_PROG_ENTRYPOINT') or { '.' } const docker_image = 'vlang-cross:latest-trixie'
const output_dir = os.abs_path(os.norm_path(os.getenv_opt('BUILD_OUTPUT_DIR') or { 'release' })) const compile_command = 'env VFLAGS="-cflags -s" DEBUG=1 ./crosscompile.vsh -release'
const skip_targets = os.getenv('BUILD_SKIP_TARGETS')
const common_vflags = os.getenv_opt('BUILD_COMMON_VFLAGS') or { '-prod' }
const common_cflags = os.getenv_opt('BUILD_COMMON_CFLAGS') or { '-static' }
const debug = os.getenv('DEBUG')
const vexe = @VEXE
/* const help_text = '
SETTING BUILD TARGETS Options:
All build targets must be defined in the build_targets const below.
Each target is a V struct. Fields are:
name string
The target name. Prefer to use https://wiki.osdev.org/Target_Triplet
Note that target name will be used in output file name. For example
the 'linux-riscv64' becomes to 'myprog-1.2.3-linux-riscv64'. This is
very common naming scheme for compiled program distributions.
cc string
C Compiler to use e.g. '/usr/bin/gcc', 'clang', etc.
vflags []string
cflags []string
ldflags []string
Flags which will be passed to V compiler. See `v help build-c` for info.
file_ext string
Extension for produced binary file. Useful for Windows builds. file_ext
is concatenated to filename. For example for target named 'windows-amd64'
and '.exe' file_ext you will get 'progname-1.2.3-windows-amd64.exe'.
disabled bool
If true target will be disabled. Target building will be skipped. Also
target will not provided in tasks list in `./make.vsh -tasks` output.
common_vflags bool
If true, set additional flags listed in BUILD_COMMON_VFLAGS.
See `./make.vsh -help` for info or read help_text const below. Is true
by default.
common_cflags bool
The same as common_vflags, but for C compiler. Environment variable is
BUILD_COMMON_CFLAGS. Is true by default.
calculate_sha256 bool
If true, calculate SHA256 hashsum of produced binary and create new
artifact with the same name but with '.sha256' extension. File content
is the same as Linux `sha256sum` utility output. Is true by default.
See also Target sttruct definition in bottom of this file.
V'S SPECIAL ENVIRONMENT VARIABLES
VCROSS_COMPILER_NAME See vcross_compiler_name() in v.pref module.
VCROSS_LINKER_NAME See vcross_linker_name() in v.pref module.
*/
const build_targets = [
Target{
name: 'linux-amd64'
cc: 'gcc'
},
Target{
name: 'linux-arm64'
cc: 'aarch64-linux-gnu-gcc'
},
Target{
name: 'linux-armhf'
cc: 'arm-linux-gnueabihf-gcc'
},
Target{
name: 'linux-ppc64le'
cc: 'powerpc64le-linux-gnu-gcc'
},
Target{
name: 'linux-s390x'
cc: 's390x-linux-gnu-gcc'
},
Target{
name: 'linux-riscv64'
cc: 'riscv64-linux-gnu-gcc'
},
Target{
name: 'windows-amd64'
cc: 'x86_64-w64-mingw32-gcc'
vflags: ['-os', 'windows']
file_ext: '.exe'
},
Target{
// FreeBSD build for now is dynamically linked even if -cflags -static is passed.
// Also V forces the use of clang here (unless VCROSS_COMPILER_NAME envvar is set),
// so -cc value doesn't matter.
name: 'freebsd-amd64'
cc: 'clang'
vflags: ['-os', 'freebsd']
},
]
const help_text = "
Build script options:
-tasks List available tasks. -tasks List available tasks.
-help Print this help message and exit. Aliases: help, --help. -help Print this help message and exit. Aliases: help, --help.
'.trim_indent()
Build can be configured throught environment variables: if 'help' in os.args || '-help' in os.args || '--help' in os.args {
println(help_text)
BUILD_PROG_NAME Name of the compiled program. By default the name is exit(0)
parsed from v.mod.
BUILD_PROG_VERSION Version of the compiled program. By default the name
is parsed from v.mod.
BUILD_PROG_ENTRYPOINT The program entrypoint. Defaults to '.' (current dir).
BUILD_OUTPUT_DIR The directory where the build artifacts will be placed.
Defaults to './release'.
BUILD_SKIP_TARGETS List of build targets to skip. Expects comma-separated
list without whitespaces e.g. 'windows-amd64,linux-armhf'
BUILD_COMMON_VFLAGS The list of V flags is common for all targets. Expects
comma-separated list. Default is '-prod,-cross'.
BUILD_COMMON_CFLAGS Same as BUILD_COMMON_VFLAGS, but passed to underlying
C compiler. Default is '-static'.
DEBUG If set enables the verbose output as dimmed text.
".trim_indent()
fn main() {
if 'help' in os.args || '-help' in os.args || '--help' in os.args {
println(help_text)
exit(0)
}
mut context := build.context(default: 'all')
mut targets := []string{}
for build_target in build_targets {
targets << build_target.name
context.task(
name: build_target.name
help: 'Make release build for ${build_target.name} target'
run: fn [build_target] (t build.Task) ! {
make_build(build_target)!
}
should_run: fn [build_target] (t build.Task) !bool {
return is_command_present(build_target.cc)!
&& build_target.name !in skip_targets.split(',')
}
)
}
context.task(
name: 'all'
help: 'Make release builds for all target systems'
depends: targets
run: |self| true
)
context.task(
name: 'clean'
help: 'Cleanup the output dir (${output_dir})'
run: |self| cleanup()!
)
context.run()
} }
fn make_build(build_target Target) ! { mut context := build.context(default: 'release')
printdbg('Env BUILD_PROG_NAME = ${program_name}')
printdbg('Env BUILD_PROG_VERSION = ${program_version}')
printdbg('Env BUILD_PROG_ENTRYPOINT = ${program_entrypoint}')
printdbg('Env BUILD_OUTPUT_DIR = ${output_dir}')
printdbg('Env BUILD_SKIP_TARGETS = ${skip_targets.split(',')}')
printdbg('Env BUILD_COMMON_VFLAGS = ${common_vflags}')
printdbg('Env BUILD_COMMON_CFLAGS = ${common_cflags}')
os.mkdir(output_dir) or {} context.task(
name: docker_image
help: 'Build Docker image for cross-compilation'
run: |self| os.system('${docker_command} build -t ${docker_image} .')
)
artifact := os.join_path_single(output_dir, program_name + '-' + program_version + '-' + context.task(
build_target.name + build_target.file_ext) name: 'build'
printdbg('Building artifact: ${artifact}') help: 'Build binaries'
run: |self| os.system('${docker_command} run --rm -v .:/app ${docker_image} ${compile_command}')
should_run: |self| os.is_dir_empty(output_dir)
depends: [docker_image]
)
mut vargs := []string{} context.task(
if build_target.common_vflags { name: 'sha256sums'
for vflag in common_vflags.split(',') { help: 'Calculate SHA256 sums for built binaries'
if vflag != '' { run: |self| os.walk(output_dir, fn (file string) {
vargs << vflag out_file := os.abs_path(file + '.sha256')
} eprintln(term.bold('Generating: ${out_file}'))
} data := os.read_bytes(file) or { return }
} sum := sha256.sum(data)
for vflag in build_target.vflags { result := '${sum.hex()} ${os.file_name(file)}\n'
vargs << vflag os.write_file(out_file, result) or { return }
} })
vargs << ['-cc', build_target.cc] depends: ['build']
if build_target.common_cflags { )
for cflag in common_cflags.split(',') {
if cflag != '' {
vargs << ['-cflags', cflag]
}
}
}
for cflag in build_target.cflags {
vargs << ['-cflags', cflag]
}
for ldflag in build_target.ldflags {
vargs << ['-ldflags', ldflag]
}
vargs << ['-o', artifact]
vargs << program_entrypoint
execute_command(vexe, vargs, env: build_target.env)! context.task(
name: 'release'
help: 'Make release'
run: |self| true
depends: ['sha256sums']
)
if build_target.calculate_sha256 { context.task(
sha256sum_file := artifact + '.sha256' name: 'clean'
printdbg('Generating SHA256 sum: ${sha256sum_file}') help: 'Cleanup build directory (delete all build artifacts)'
file_bytes := os.read_bytes(artifact)! run: |self| os.rmdir_all(output_dir) or {}
sum := sha256.sum(file_bytes) )
result := '${sum.hex()} ${os.file_name(artifact)}\n'
printdbg('Calculated SHA256: ${result}')
os.write_file(sha256sum_file, result)!
}
}
fn cleanup() ! { context.run()
printdbg('Try to delete ${output_dir} recursively...')
os.rmdir_all(output_dir) or {
if err.code() == 2 {
printdbg('${output_dir} does not exists')
} else {
return err
}
}
printdbg('Cleanup done')
}
// Helper functions
fn vmod_name() string {
if manifest := vmod.decode(@VMOD_FILE) {
return manifest.name
}
return 'NAMEPLACEHOLDER'
}
fn vmod_version() string {
if manifest := vmod.decode(@VMOD_FILE) {
return manifest.version
}
return 'VERSIONPLACEHOLDER'
}
fn is_command_present(cmd string) !bool {
if os.exists_in_system_path(cmd) {
return true
}
if os.is_executable(os.abs_path(os.norm_path(cmd))) {
return true
}
printwarn('Command ${term.bold(cmd)} is not found')
return false
}
@[params]
struct CommandOptions {
env map[string]string
}
fn execute_command(executable string, args []string, opts CommandOptions) ! {
path := os.find_abs_path_of_executable(executable) or { os.norm_path(executable) }
printdbg("Run '${path}' with arguments: ${args}")
mut proc := os.new_process(path)
proc.set_args(args)
proc.set_environment(maps.merge(os.environ(), opts.env))
proc.set_work_folder(os.getwd())
proc.run()
proc.wait()
if proc.status == .exited && proc.code != 0 {
return error('Command ${term.bold(path)} exited with non-zero code ${proc.code}')
}
}
fn printdbg(s string) {
if debug !in ['', '0', 'false', 'no'] {
eprintln(term.dim(s))
}
}
fn printwarn(s string) {
eprintln(term.bright_yellow(s))
}
struct Target {
name string
cc string
vflags []string
cflags []string
ldflags []string
file_ext string
env map[string]string
common_vflags bool = true
common_cflags bool = true
calculate_sha256 bool = true
}