commit dfffab28abe79a736e75d0225f17b09ad58717df Author: ge Date: Sat Sep 20 00:24:17 2025 +0300 init diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..01072ca --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.v] +indent_style = tab diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..9a98968 --- /dev/null +++ b/.gitattributes @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..20de96f --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..61d16d9 --- /dev/null +++ b/Dockerfile @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..70a617d --- /dev/null +++ b/README.md @@ -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 diff --git a/main.v b/main.v new file mode 100644 index 0000000..edf2574 --- /dev/null +++ b/main.v @@ -0,0 +1,5 @@ +module main + +fn main() { + println('Hello World!') +} diff --git a/make.vsh b/make.vsh new file mode 100755 index 0000000..8dc96d0 --- /dev/null +++ b/make.vsh @@ -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 +} diff --git a/v.mod b/v.mod new file mode 100644 index 0000000..58bdf0f --- /dev/null +++ b/v.mod @@ -0,0 +1,7 @@ +Module { + name: 'helloworld' + description: 'V Cross-compilation example' + version: '1.0.0' + license: 'MIT' + dependencies: [] +}