From dfffab28abe79a736e75d0225f17b09ad58717df Mon Sep 17 00:00:00 2001 From: ge Date: Sat, 20 Sep 2025 00:24:17 +0300 Subject: [PATCH] init --- .editorconfig | 8 ++ .gitattributes | 8 ++ .gitignore | 29 +++++ Dockerfile | 39 +++++++ README.md | 85 ++++++++++++++ main.v | 5 + make.vsh | 297 +++++++++++++++++++++++++++++++++++++++++++++++++ v.mod | 7 ++ 8 files changed, 478 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 main.v create mode 100755 make.vsh create mode 100644 v.mod 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: [] +}