Cross compilation

Cross compilation is supported for Windows, Linux and FreeBSD.

To cross compile your project simply run:

v -os windows .

or

v -os linux .

or

v -os freebsd .

On macOS, -os linux with the default C compiler or Clang uses the bundled amd64 Linux sysroot. V automatically enables $if cross_compile ? for this toolchain so modules such as db.pg select the bundled Linux headers and compatibility sources.

[!NOTE] Cross-compiling a Windows binary on a Linux or macOS machine requires the GNU C compiler for MinGW-w64 (targeting Win64) to first be installed. Without -arch and -cc, -os windows builds an amd64 executable with x86_64-w64-mingw32-gcc, also on an arm64 machine.

For Ubuntu/Debian-based distributions:

sudo apt install gcc-mingw-w64-x86-64

For Arch based distributions:

sudo pacman -S mingw-w64-gcc

For macOS with Homebrew:

brew install mingw-w64

(Cross compiling for macOS is temporarily not possible.)

If you don't have any C dependencies, that's all you need to do. This works even when compiling GUI apps using the ui module or graphical apps using gg.

If you need a custom cross compiler, pass -cc <compiler> for one build, or set VCROSS_COMPILER_NAME in your environment. Example:

v -os linux -cc cosmocc .

You will need to install Clang, LLD linker, and download a zip file with libraries and include files for Windows and Linux. V will provide you with a link.

Portable C output (-os cross)

-os cross is not a platform. It asks for portable C, i.e. C that is not tied to one OS, architecture or C compiler, so that a single generated file can be compiled on any of them. It is how V's own bootstrap snapshot vc/v.c is produced, and it only makes sense with -o file.c:

v -os cross -o /tmp/v.c cmd/v
cc -o v_from_c /tmp/v.c -lm -lpthread

The -cross flag asks for the same output, but as a modifier that combines with an explicit target, which is how the Windows bootstrap snapshot is produced:

v -cross -os windows -cc msvc -o /tmp/v_win.c cmd/v

Either spelling also turns on the cross and no_backtrace custom defines, so that the $if cross ? guards in the standard library select their portable path instead of a platform syscall.

In this mode V does not decide a target-dependent $if while generating. It keeps every branch and emits the condition as a C preprocessor guard, leaving the choice to whichever C compiler builds the file:

$if linux { linux_only() } $else { everywhere_else() }

becomes

#if (defined(__linux__) && !defined(__ANDROID__)) linux_only(); #else everywhere_else(); #endif

Conditions that do not depend on the target - $if prealloc, $if debug, -d values - are still resolved while generating, exactly as in an ordinary build. #includes written inside a $if, or carrying a target prefix such as #include linux <sys/timerfd.h>, are guarded the same way, and headers or C sources shipped alongside your code are embedded into the output instead of being referenced by a path that will not exist on the machine that compiles it.

What is not portable, and is therefore decided while generating, for the host V runs on:

  • A $if used as an expression. Its branches may have different types - for example closure_thunk in vlib/builtin/closure is a differently sized fixed array per architecture - which no guard around an expression can express. Where such a value has to follow the target, select it with statement-level $ifs in a function used under $if cross ?, as vlib/builtin does for min_int, max_int and the closure thunks.
  • A $if at file scope holding declarations. A function, type, constant or global cannot be wrapped in #if by the backend, so only the host's branch is emitted. Directives (#include, #flag) written at file scope are kept from every branch and guarded, which is what makes the headers portable.
  • Environment specific files. A module split into x_linux.c.v and x_darwin.c.v contributes only the generating host's variant, so generate the portable C on the platform whose variants are the portable ones.
  • The literal ranges. They are checked for the generating target, so an int literal that only fits 64 bits is accepted, and truncated when the output is compiled for a 32-bit target. Use i64 for such values.

The pointer width itself is left to the C compiler. V's int is spelled __v_int, a typedef of i64 or i32 that the output defines from the compiler's target, and the map callbacks for int and pointer keys and the default thread stack size follow that width as well.

The compiler's own snapshot (v -cross cmd/v, which vc/v.c is) writes the libc declarations it needs itself, with the layouts of 64-bit targets. On 32-bit targets the C libraries disagree on those layouts (glibc and musl differ in time_t, off_t and struct stat), so the snapshot also carries a variant that includes the system headers. The C compiler picks it on 32-bit targets; compile with -D__V_HEADERLESS_LIBC=0 to use it on a 64-bit one too.