mirror of
https://github.com/gechandesu/v-cross-compilation-example.git
synced 2026-01-02 14:39:35 +03:00
init
This commit is contained in:
8
.editorconfig
Normal file
8
.editorconfig
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.v]
|
||||||
|
indent_style = tab
|
||||||
8
.gitattributes
vendored
Normal file
8
.gitattributes
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
* text=auto eol=lf
|
||||||
|
*.bat eol=crlf
|
||||||
|
|
||||||
|
*.v linguist-language=V
|
||||||
|
*.vv linguist-language=V
|
||||||
|
*.vsh linguist-language=V
|
||||||
|
v.mod linguist-language=V
|
||||||
|
.vdocignore linguist-language=ignore
|
||||||
29
.gitignore
vendored
Normal file
29
.gitignore
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Binaries for programs and plugins
|
||||||
|
make
|
||||||
|
v-cross-compilation-example
|
||||||
|
main
|
||||||
|
helloworld
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
*.dll
|
||||||
|
|
||||||
|
# Ignore binary output folders
|
||||||
|
bin/
|
||||||
|
|
||||||
|
# Ignore common editor/system specific metadata
|
||||||
|
.DS_Store
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.iml
|
||||||
|
|
||||||
|
# ENV
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Web and database
|
||||||
|
*.db
|
||||||
|
*.js
|
||||||
|
|
||||||
|
# Build artifacts
|
||||||
|
release/
|
||||||
39
Dockerfile
Normal file
39
Dockerfile
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
FROM debian:trixie
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install --assume-yes --no-install-recommends --no-install-suggests \
|
||||||
|
ca-certificates \
|
||||||
|
git \
|
||||||
|
build-essential && \
|
||||||
|
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 && \
|
||||||
|
cd /opt/v && \
|
||||||
|
make && \
|
||||||
|
/opt/v/v symlink && \
|
||||||
|
v version
|
||||||
|
|
||||||
|
# See https://wiki.debian.org/CrossCompiling
|
||||||
|
RUN dpkg --add-architecture arm64 && \
|
||||||
|
dpkg --add-architecture armhf && \
|
||||||
|
dpkg --add-architecture s390x && \
|
||||||
|
dpkg --add-architecture ppc64el && \
|
||||||
|
dpkg --add-architecture riscv64 && \
|
||||||
|
apt-get update && \
|
||||||
|
apt-get install --assume-yes --no-install-recommends --no-install-suggests \
|
||||||
|
crossbuild-essential-arm64 \
|
||||||
|
crossbuild-essential-armhf \
|
||||||
|
crossbuild-essential-s390x \
|
||||||
|
crossbuild-essential-ppc64el \
|
||||||
|
crossbuild-essential-riscv64 \
|
||||||
|
gcc-mingw-w64-x86-64 \
|
||||||
|
clang lld && \
|
||||||
|
apt-get clean && rm -rf /var/cache/apt/archives/* && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
USER 1000:1000
|
||||||
|
|
||||||
|
ENV VMODULES=/tmp/.vmodules
|
||||||
85
README.md
Normal file
85
README.md
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
# V Cross-compilation Example
|
||||||
|
|
||||||
|
This example shows how to build statically linked binaries for different OS
|
||||||
|
and platforms for [V](https://vlang.io) programs.
|
||||||
|
|
||||||
|
The build is performed by the make.vsh script in V. I think it could be
|
||||||
|
simplified further, but the current version LGTM. Please read the comments
|
||||||
|
inside make.vsh for details.
|
||||||
|
|
||||||
|
The build is done inside a docker container with Debian 13 Trixie. Host is
|
||||||
|
`x86_64` Linux.
|
||||||
|
|
||||||
|
Produced binaries:
|
||||||
|
|
||||||
|
* Linux: `amd64`, `arm64`, `arm32` (`armhf`), `ppc64le`, `s390x`, `riscv64`
|
||||||
|
* Windows: `amd64`
|
||||||
|
* FreeBSD: `amd64`
|
||||||
|
|
||||||
|
The example programm is just `Hello World!`. For complex programs you may need
|
||||||
|
to add more dependencies in build container.
|
||||||
|
|
||||||
|
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
|
||||||
|
and operating systems.
|
||||||
|
|
||||||
|
Build:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker build . -t vlang-cross:latest-trixie
|
||||||
|
```
|
||||||
|
|
||||||
|
The container image is large (a little over 2GiB) due to the number of libraries
|
||||||
|
required for cross-compilation. The size could actually be reduced, but that's
|
||||||
|
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).
|
||||||
|
|
||||||
|
Start cross-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
|
||||||
|
|
||||||
|
* `v help build`
|
||||||
|
* `v help build-c`
|
||||||
|
* https://docs.vlang.io/cross-compilation.html
|
||||||
|
* https://wiki.debian.org/CrossCompiling
|
||||||
|
* https://en.wikipedia.org/wiki/Cross_compiler#GCC_and_cross_compilation
|
||||||
|
* https://clang.llvm.org/docs/CrossCompilation.html
|
||||||
297
make.vsh
Executable file
297
make.vsh
Executable file
@@ -0,0 +1,297 @@
|
|||||||
|
#!/usr/bin/env -S v run
|
||||||
|
|
||||||
|
import build
|
||||||
|
import crypto.sha256
|
||||||
|
import os
|
||||||
|
import term
|
||||||
|
import v.vmod
|
||||||
|
|
||||||
|
const program_name = os.getenv_opt('BUILD_PROG_NAME') or { vmod_name() }
|
||||||
|
const program_version = os.getenv_opt('BUILD_PROG_VERSION') or { vmod_version() }
|
||||||
|
const program_entrypoint = os.getenv_opt('BUILD_PROG_ENTRYPOINT') or { '.' }
|
||||||
|
const output_dir = os.abs_path(os.norm_path(os.getenv_opt('BUILD_OUTPUT_DIR') or { 'release' }))
|
||||||
|
const skip_targets = os.getenv('BUILD_SKIP_TARGETS')
|
||||||
|
const common_vflags = os.getenv_opt('BUILD_COMMON_VFLAGS') or { '-prod,-cross' }
|
||||||
|
const common_cflags = os.getenv_opt('BUILD_COMMON_CFLAGS') or { '-static' }
|
||||||
|
const debug = os.getenv('DEBUG')
|
||||||
|
const vexe = @VEXE
|
||||||
|
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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, so -cc value doesn't matter.
|
||||||
|
name: 'freebsd-amd64'
|
||||||
|
cc: 'clang'
|
||||||
|
vflags: ['-os', 'freebsd']
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const help_text = "
|
||||||
|
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.
|
||||||
|
".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: 'release')
|
||||||
|
mut targets := []string{}
|
||||||
|
for build_target in build_targets {
|
||||||
|
if build_target.disabled {
|
||||||
|
printdbg('Build target ${build_target.name} is disabled in build script')
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
targets << build_target.name
|
||||||
|
context.artifact(
|
||||||
|
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: 'release'
|
||||||
|
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()!
|
||||||
|
should_run: |self| true
|
||||||
|
)
|
||||||
|
context.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_build(build_target Target) ! {
|
||||||
|
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 {}
|
||||||
|
|
||||||
|
artifact := os.join_path_single(output_dir, program_name + '-' + program_version + '-' +
|
||||||
|
build_target.name + build_target.file_ext)
|
||||||
|
printdbg('Building artifact: ${artifact}')
|
||||||
|
|
||||||
|
mut vargs := []string{}
|
||||||
|
if build_target.common_vflags {
|
||||||
|
for vflag in common_vflags.split(',') {
|
||||||
|
if vflag != '' {
|
||||||
|
vargs << vflag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for vflag in build_target.vflags {
|
||||||
|
vargs << vflag
|
||||||
|
}
|
||||||
|
vargs << ['-cc', build_target.cc]
|
||||||
|
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)!
|
||||||
|
|
||||||
|
if build_target.calculate_sha256 {
|
||||||
|
sha256sum_file := artifact + '.sha256'
|
||||||
|
printdbg('Generating SHA256 sum: ${sha256sum_file}')
|
||||||
|
file_bytes := os.read_bytes(artifact)!
|
||||||
|
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() ! {
|
||||||
|
path := output_dir
|
||||||
|
if os.is_dir(path) {
|
||||||
|
printdbg('Deleting the ${path} dir recursively...')
|
||||||
|
os.rmdir_all(path)!
|
||||||
|
printdbg('${path} is deleted')
|
||||||
|
} else {
|
||||||
|
printdbg('${path} does not exists or is not directory')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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
|
||||||
|
|
||||||
|
disabled bool
|
||||||
|
common_vflags bool = true
|
||||||
|
common_cflags bool = true
|
||||||
|
calculate_sha256 bool = true
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user