SDK Projects
Starter
By now you have entered some OCaml code into utop
but some key features
were missing that you can get by creating/using a local project.
A local project is a folder that contains your source code, one or more sets of packages (other people’s code) and one or more build directories to store your compiled code and applications.
By using a local project you will be able to:
Install other people’s code packages
Edit your source code in an IDE
Build your source code into applications or libraries
This is easiest to see with an example.
Open PowerShell (press the Windows key ⊞, type “PowerShell” and then Open
Windows PowerShell
).Run the following in PowerShell:
PS1> cd ~\DiskuvOCamlProjects PS1> git clone --recursive https://gitlab.com/diskuv/diskuv-ocaml-starter.git
You now have a local project in ~\DiskuvOCamlProjects\diskuv-ocaml-starter
!
We can initialize an Opam repository, assemble an Opam
switch and compile the source code all by running the single build-dev
target:
PS1> cd ~\DiskuvOCamlProjects\diskuv-ocaml-starter
PS1> ./makeit build-dev DKML_BUILD_TRACE=ON
We turned on tracing (DKML_BUILD_TRACE=ON
) so you could see what is happening;
the three steps of build-dev
are:
Initialize an Opam repository. This takes several minutes but only needs to be done once per user (you!) per machine.
Assemble (create) an Opam switch by compiling all the third-party packages you need. Any new packages you add to
.opam
files will be added to your Opam switch. This can take tens of minutes but only needs to be done once per Local Project.Compile your source code. This is usually in the 0-5 seconds range unless your project is large or uses C code. There is a special Makefile target called
quickbuild-dev
that skips the first two steps and only compiles your source code.
The starter application is the Complete Program
example from the Real World OCaml book. Let us run it.
You will enter the numbers 1
, 2
, 3
and 94.5
, and then stop the program by
typing Ctrl-C or Enter + Ctrl-Z:
PS1> _build/default/bin/main.exe
> 1
> 2
> 3
> 94.5
> Total: 100.5
Recap: You fetched a SDK Project, built its code and all of its dependencies, and then ran the resulting application!
In your own projects you will likely be making edits, and then building, and then repeating
the edit and build steps over and over again. Since you already did build-dev
once, use the
following to “quickly” build your SDK Project:
PS1> ./makeit quickbuild-dev
The next section Integrated Development Environment (IDE) will go over how to automatically and almost instantaneously build your code whenever you make an edit.
Visual Studio Code Development
Launch Visual Studio Code
Open the folder (File > Open Folder; or Ctrl+K Ctrl+O)
%USERPROFILE%\DiskuvOCamlProjects\diskuv-ocaml-starter
Open a Terminal (Terminal > New Terminal; or Ctrl+Shift+`). In the terminal type:
[diskuv-ocaml-starter]$ ./makeit dkml-devmode >> while true; do \ >> DKML_BUILD_TRACE=OFF vendor/diskuv-ocaml/runtime/unix/platform-dune-exec.sh -p dev -b Debug \ >> build --watch --terminal-persistence=clear-on-rebuild \ >> bin lib test ; \ >> sleep 5 || exit 0; \ >> done >> Scanned 0 directories >> fswatch args = (recursive=true; event=[Removed; Updated; Created]; >> include=[]; >> exclude=[4913; /#[^#]*#$; ~$; /\..+; /_esy; /_opam; /_build]; >> exclude_auto_added=[\\#[^#]*#$; \\\..+; \\_esy; \\_opam; \\_build; \\\.git; \\_tmp]; >> paths=[.]) >> inotifywait loc = C:\Users\beckf\AppData\Local\Programs\DiskuvOCaml\1\tools\inotify-win\inotifywait.exe >> inotifywait args = [--monitor; --format; %w\%f; --recursive; --event; delete,modify,create; --excludei; 4913|/#[^#]*#$|~$|/\..+|/_esy|/_opam|/_build|\\#[^#]*#$|\\\..+|\\_esy|\\_opam|\\_build|\\\.git|\\_tmp; .] >> Done: 0/0 (jobs: 0)===> Monitoring Z:\source\diskuv-ocaml-starter -r*.* for delete, modify, create >> Success, waiting for filesystem changes...
Keep this Terminal open for as long as you have the local project (in this case
diskuv-ocaml-starter
) open. It will watch your local project for any changes you make and then automatically build them.The automatic building uses Dune’s watch mode; its change detection and compile times should be almost instantaneous for most projects.
Open another Terminal. In this terminal you can quickly test some pieces of your code. To test
lib/dune
andlib/terminal_color.ml
which come directly from the Variants chapter of the Real World OCaml book you would type:PS Z:\source\diskuv-ocaml-starter> ./makeit shell-dev >> diskuv-ocaml-starter$
[diskuv-ocaml-starter]$ dune utop > ──────────┬─────────────────────────────────────────────────────────────┬────────── > │ Welcome to utop version 2.8.0 (using OCaml version 4.12.0)! │ > └─────────────────────────────────────────────────────────────┘ > > Type #utop_help for help about using utop. > > ─( 06:26:11 )─< command 0 >─────────────────────────────────────────{ counter: 0 }─ > utop #
utop #> #show Starter;; > module Starter : sig module Terminal_color = Starter.Terminal_color end utop #> #show Starter.Terminal_color;; > module Terminal_color = Starter.Terminal_colormodule Terminal_color : > sig > type basic_color = > Black > | Red > | Green > | Yellow > | Blue > | Magenta > | Cyan > | White > val basic_color_to_int : basic_color -> int > val color_by_number : int -> string -> string > val blue : string > end utop #> open Stdio;; utop #> open Starter.Terminal_color;; utop #> printf "Hello %s World!\n" blue;; > Hello Blue World! > - : unit = () utop #> #quit;;
Open the source code
bin/main.ml
andlib/terminal_color.ml
in the editor. When you hover over the text you should see type information popup.Change the indentation of
bin/main.ml
andlib/terminal_color.ml
. Then press Shift + Alt + F (or go to View > Command Palette and type “Format Document”). You should see your code reformatted.
Finished?
Warning
The remainder of the SDK Projects documentation is not ready for consumption. And we are missing a tool to make your own SDK Project. Stop here!
Build Process
There are a hierarchy of build tools that are used to build an SDK project:
CMake controls almost all of the build process.
First the script ./makeit generate-XX-on-YY
runs a GNU Makefile script that
selects the build tool (Ninja, Visual Studio, xcode, etc.) and then invokes
the generation phase of CMake. During this phase CMake will:
create the build directory
copy the source code into the build directory
create configuration files for the chosen build tool
The chosen build tool can then be invoked. For example on Windows the Visual Studio build tool is used and you can open the “solution” in Visual Studio and then build the project from within Visual Studio.
Anytime after when you edit the source code one of two things can happen:
You edit the project metadata in the
CMakeLists.txt
files: CMake will have written intelligence into the build tool configuration files so that when any project metadata has changed the CMake generation phase will be rerun to update the build tool.You edit OCaml or C code, or edit
dune
files: The chosen build tool will notice your changes and incrementally compile the code if you build the porject.
You can go back and forth from OCaml to C because OCaml packages are treated as CMake targets, and DKSDK has added logic to CMake to wire together C and OCaml targets.
Directory Layout
diskuv-ocaml-starter
is an example of the standard layout which looks like:
.
├── bin
│ ├── dune
│ └── main.ml
├── build
│ ├── _tools
│ │ └── dev
│ └── dev
│ └── Debug
├── buildconfig
│ └── dune
│ ├── .gitignore
│ ├── dune.env.workspace.inc
│ ├── executable
│ └── workspace
├── dune
├── dune-project
├── dune-workspace
├── lib
│ ├── dune
│ └── terminal_color.ml
├── LICENSE.txt
├── makeit
├── makeit.cmd
├── Makefile
├── README.md
├── opam
├── test
│ ├── dune
│ └── starter.ml
└── vendor
├── diskuv-ocaml
└── diskuv-sdk
TODO Explanation of each directory and file.
Makefile
Configuration
The Diskuv OCaml specific configuration for your local project is at the top of your
Makefile
.
Here is an example from the diskuv-ocaml-starter
local project:
#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#
# RESERVED FOR DISKUV OCAML #
# BEGIN CONFIGURATION #
# #
# Place this section before the first target (typically 'all:') #
#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#
# The subdirectory for the 'diskuv-ocaml' git submodule
DKML_DIR = vendor/diskuv-ocaml
# Verbose tracing of each command. Either ON or OFF
DKML_BUILD_TRACE = OFF
# The source directories. No platform-specific source code belongs here.
OCAML_SRC_CROSSPLATFORM = bin lib
# The test directories. No platform-specific source code belongs here.
OCAML_TEST_CROSSPLATFORM = test
# The names of the Windows-specific Opam packages (without the .opam suffix), if any.
OPAM_PKGS_WINDOWS =
# The source directories containing Windows-only source code, if any.
OCAML_SRC_WINDOWS =
# The test directories for Windows source code, if any.
OCAML_TEST_WINDOWS =
#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#
# END CONFIGURATION #
# RESERVED FOR DISKUV OCAML #
#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#
Targets
The Diskuv OCaml specific targets for your local project are at the bottom of your
Makefile
.
Here is an example from the diskuv-ocaml-starter
local project:
#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#
# RESERVED FOR DISKUV OCAML #
# BEGIN TARGETS #
# #
# Place this section anywhere after the `all` target #
#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#
include $(DKML_DIR)/runtime/unix/standard.mk
#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#
# END TARGETS #
# RESERVED FOR DISKUV OCAML #
#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#
buildconfig/dune/
.
└── buildconfig
└── dune
├── .gitignore
├── dune.env.workspace.inc
├── executable
│ ├── 1-base.link_flags.sexp
│ ├── 2-dev-all.link_flags.sexp
│ ├── 3-all-Debug.link_flags.sexp
│ ├── 3-all-Release.link_flags.sexp
│ ├── 3-all-ReleaseCompatFuzz.link_flags.sexp
│ ├── 3-all-ReleaseCompatPerf.link_flags.sexp
│ ├── 4-dev-Debug.link_flags.sexp
│ ├── 4-dev-Release.link_flags.sexp
│ ├── 4-dev-ReleaseCompatFuzz.link_flags.sexp
│ └── 4-dev-ReleaseCompatPerf.link_flags.sexp
└── workspace
├── 1-base.ocamlopt_flags.sexp
├── 2-dev-all.ocamlopt_flags.sexp
├── 3-all-Debug.ocamlopt_flags.sexp
├── 3-all-Release.ocamlopt_flags.sexp
├── 3-all-ReleaseCompatFuzz.ocamlopt_flags.sexp
├── 3-all-ReleaseCompatPerf.ocamlopt_flags.sexp
├── 4-dev-Debug.ocamlopt_flags.sexp
├── 4-dev-Release.ocamlopt_flags.sexp
├── 4-dev-ReleaseCompatFuzz.ocamlopt_flags.sexp
└── 4-dev-ReleaseCompatPerf.ocamlopt_flags.sexp
Setting Up An Existing Git Repository As a SDK Project
The directory structure does _not_ need to look like the standard layout.
The requirements are:
Use
diskuv-ocaml
as a submodule, as in:PS1> git submodule add ` https://gitlab.com/diskuv/diskuv-ocaml.git ` vendor/diskuv-ocaml
You can place the submodule in any directory (not just
vendor
) but the basename should bediskuv-ocaml
.There must be a
dune-project
in an ancestor directory of thediskuv-ocaml
Git submodule. For example, it is fine to have:.git/ .gitmodules a/ b/ dune-project src/ c/ d/ diskuv-ocaml/
TODO Complete.
Upgrading
Run:
PS1> .\vendor\diskuv-ocaml\runtime\windows\upgrade.ps1
If there is an upgrade of Diskuv OCaml
available it will automate as much as possible,
and if necessary give you further instructions to complete the upgrade.
Static or Dynamic Linking
For Linux we use static linking, with no dependency on even the system C runtime library.
There is little benefit to doing static linking on Windows. Windows has
a standard installer (.msi
or setup*.exe
) that can install any
necessary DLLs. The only benefit for reducing the DLL dependencies are
when distributing a Windows library so that library users do not
need to bundle the DLLs. However, it is a terrible idea to stop relying
on the Windows system libraries, especially the C runtime, since two C
runtimes should not co-exist in the same process space.
Android and macOS are similar to Windows in that they have standardized installers that can bundle any shared libraries.
The OCaml compiler produces static objects and static libraries unless
you give the -shared
option to ocamlopt. However OCaml executables
are dynamically linked with the C libraries of the OCaml package
dependencies unless -ccopt static
is given to ocamlopt.
Build Platforms
We use Linux based containers (including Windows WSL2 and untested Docker on macOS) as the build host because:
wine
is only available in the x86 and x86_64 architectures as of July 2021. We could compilewine
(perhaps most easily for macOS) but at the moment it is not worth the effort since Docker (aka Linux containers) is available on most platforms including macOS.
Dev and Target Platforms
All platforms except dev
are target platforms. Target platforms
are built in a Docker sandbox and may have CPU emulation to get
different CPU architectures to work.
If you have continuous integration hardware, use the target platforms!
The dev
platform is your own development machine. There are key
differences from the target platforms:
When the dev platform is initialized through
make init-dev
extra software is downloaded to support IDEs.We do our best to avoid any need for running Docker. Why? Docker, especially on Windows (and probably Apple M1s), has some difficult to work around limitations like having to switch between Windows and Linux containers, not having critical packages available for non-Linux containers, and oftentimes being incompatible with other virtualization (most of the Hyper-V incompatibilites have been fixed on Windows).
Platform |
Description |
---|---|
dev |
Your own dev machine. |
linux_x86_64 |
AMD/Intel 64-bit Linux. Static linking |
Warning
32-bit Windows
TLDR: 32-bit executables with “install”, “setup” or “update” in their filename, when run from MSYS2, will fail.
These same executables when run from PowerShell or the Command Prompt will pop up the “Do you want to allow this app from an unknown publisher to make changes to your device?” User Account Control. However this logic does not seem to be available in MSYS2 (or Cygwin), so in MSYS2 you get a Permission Denied.
Solutions:
Change the executable filename if that is possible.
Run as Administrator
Disable the “User Account Control: Detect application installations and prompt for elevation” policy setting and then reboot. See https://docs.microsoft.com/en-us/windows/security/identity-protection/user-account-control/user-account-control-security-policy-settings#user-account-control-detect-application-installations-and-prompt-for-elevation
Build Types
Build Type |
Description |
---|---|
Debug |
Slightly optimized code with debugging symbols |
Release |
Fully optimized [1] code. Dune builds with analog of
|
ReleaseC ompatFuz z |
Mostly optimized [1] [2] code with compatibility for american fuzzy lop (AFL) |
ReleaseC ompatPer f |
[1]: Release
, ReleaseCompatFuzz
and ReleaseCompatPerf
all
use the Flamba
optimizations
with the highest -O3
optimization level.
[2]: ReleaseCompatFuzz
changes the native code so it can be tested
with automated security fuzz
testing.
OCaml will be configured with
afl-instrument
which will cause all OCaml executables to be instrumented for fuzz
testing.
a bit slower (~3-5%) but easy to do performance probing with Perf.
With CMake the build types are available in the CMAKE_CONFIGURATION_TYPES or CMAKE_BUILD_TYPE variables.
Each build type has a corresponding Visual Studio Code CMake Tools Variant.
Makefile Targets
We use Makefile targets to help you keep track of everything.
In Windows you use the command
.\make
rather thanmake
. Wherever you seemake
in this document you should replace it with.\make
.
For example to clean up builds:
make clean
cleans all builds from all target platforms (including the dev platform) and cleans all tools (use with caution!)make clean-dev-all
cleans all builds from the dev platform and tools specific to the dev platformmake clean-all-Release
cleans the Release build from all the target platforms (including the dev platform)make clean-linux_x86_64-all
cleans all builds from the linux_x86_64 target platform and tools specific to the target platformmake clean-linux_x86_64-Release
clean the Release build from the linux_x86_64 target platform
There are many variations of make build
all of which default to the
Debug build unless you explicitly specify:
make build
builds all target platforms and all build types (but since you will likely never want to do this as a safeguard you must runmake build FORCE_CRAZY_BUILD=ON
)make build-all
builds the Debug build for all target platformsmake build-dev
builds the Debug build for the dev platformmake build-linux_x86_64
builds the Debug build for the linux_x86_64 target platformmake build-dev-Release
builds the Release build for the dev platformmake build-all-Release
builds the Release build for all the target platformsmake build-linux_x86_64-Release
build the Release build for the linux_x86_64 target platform
When you don’t edit any of the Docker files and you have done at least
one make build-*
you can subsequently use make quickbuild-*
(which skips Docker building and installing tools and Opam dependencies)
for rapid development.
Building will install any new dependencies you list in your .opam
files as long as you commit those files before running any
make build-*
.
Building should be performed before testing. You can do:
make build-XXX
followed by amake test-XXX
(ex.make build-dev
thenmake test-dev
)make build-XXX test-XXX
(ex.make build-dev test-dev
)make test
which will test everything that has already been built (useful when you are doing agile points burn-down development)
Use make report
to see what has been built and all of its compiler
flags. If you need to send in a bug report include the output of
``make report``.
Build Directories
The directory structure is the same regardless whether Windows or Linux is used as the development platform, unless noted otherwise.
_build
build
_tools
common
- Tools shared across all platforms, if anylocal
- Shared platform local installation folderbin
- Executables and scripts here are added to the build PATH
opam-bootstrap
- Native Windows version of Opam, on Windows build machines onlybin
- Install location containing Opam executable and shared DLLs
dev
- Tools for the dev platformlocal
- Dev platform local installation folderbin
- Executables and scripts here are added to the build PATH if the build is for the dev platformdune
- Drop-in replacement fordune
opam
- Drop-in replacement foropam
PLATFORM
- Tools for a specific target platformlocal
- Target platform local installation folderbin
- Executables and scripts here are added to the build PATH if the build is for the specific target platformdune
- Drop-in replacement fordune
opam
- Drop-in replacement foropam
Build PATH manipulation is done in ``.scriptsunixwithin-dev.sh`` and ``contextslinux-buildsandbox-entrypoint.sh``
OCaml
Opam Packages
We use Opam as the package manager for OCaml code.
Each target platform has its own Opam root
located at build/_tools/TARGET_PLATFORM/opam-root
except the dev
platform which uses the default Opam root ~/.opam
.
Each combination of target platform and build
type has its own Opam switch located at
build/TARGET_PLATFORM/BUILD_TYPE/_opam
.
Dune Builds
OCaml code is built with Dune.
When using make build-dev
, which is the target used by the IDE
Support, or make build-dev-*
all Dune build
artifacts are built. However all other make build-*
targets will
build only the public artifacts that will be installed. This corresponds
to the `all
alias for the dev platform and the install
alias for
the reproducible container
platforms <https://dune.readthedocs.io/en/stable/usage.html#built-in-aliases>`__.
We expect a development lifecycle that looks like:
You develop new executables and new libraries, build it and test it from your IDE and from the command line with
make build-dev test-dev
When the new executables and libraries are ready to be cross-platform tested, you can add a
(public_name ...)
to your executable stanza and/or your library stanza. Any support files they need at runtime should be present with a install stanza or by defining a site.
The scripts/unix/platform-dune-exec.sh
script is used to launch all
Dune builds:
It sets the Dune profile to
TARGET_PLATFORM-BUILD_TYPE
(ex.dune --profile linux_x86_64-Release ...
) so that Makefile, CMake and Dune can share the target platform and build type. By default the profile isdev-Debug
which is the “profile” setting indune-workspace
so that when you or and IDE runsdune ...
without platform-dune-exec.sh Dune will use the Debug settings.It sets the build directory (ex.
dune --build-dir XXX ...
) to place the Dune build files in:the standard
_build
directory for thedev-Debug
platform.build/dev/BUILD_TYPE/_dune
for all non-Debug
dev platformsbuild/TARGET_PLATFORM/BUILD_TYPE/_dune
for a reproducible container platformTyping
dune clean
from the command line will only clean thedev-Debug
target! Since it can be insanely expensive to rebuild other CPU architectures through CPU emulation and compile with the Release optimizations, this is a good side-effect we intend to keep. Instead use one of severalmake clean-*
targets described in the Makefile Targets sections
dune.env.workspace.inc
We provide Dune our target platform and build
type specific compiler settings by including
dune.env.workspace.inc
in our dune
files. For example the
ocamlopt
native code compiler will use the -O3
flag when the
build type is Release. dune.env.workspace.inc
is
an autogenerated file produced by make dune.env.workspace.inc
and
which gets generated automatically for any make init-dev
,
make build-dev
or make build-dev-Debug
.
make dune.env.workspace.inc
is responsible for generating an empty
compiler setting file in cmake/dune/*/*.sexp
if there is a
permutation of target platform and build
type missing. But ultimately CMake is responsible
for placing it own C compiler settings into some critical .sexp files
(in particular the *all*.sexp
) files.
You are welcome to tweak any compiler setting file that does not have
a warning that it is autogenerated by CMake. For your and others sanity
please include a comment and a date on a separate line for any tweak in
a .sexp
file. An example:
(-ccopt -static) ; Used in dune.env.workspace.inc.
; 2021-08-04: yourname@ - Static compilation makes executables portable across Linux.
That will make it easy to search for any tweaks (ex.
grep -C10 '^[^(]' buildconfig/dune/*/*.sexp
).
The compiler setting
.sexp
files are numbered in order of precedence. So1-*.ocamlopt_flags.sexp
are included before2-*.ocamlopt_flags.sexp
when Dune creates the flags for theocamlopt
native code compiler.In VS Code you can set the Language Mode to
dune (dune)
for syntax highlighting. Scheme and Lisp syntax highlighting should also work in other IDEs.
IDE Support
An IDE with type introspection is critical to develop OCaml source code.
IDEs like Visual Studio Code detect the presence of a Dune-based project
(likely just checking for a dune
file) and expect Dune to provide
Merlin based type
introspection and auto-completion.
Dune is able to provide Merlin based type introspection and auto-completion.
dune printenv --verbose
can be used to tell if the current Dune
context is providing Merlin introspection and which Opam switch will be
introspected:
text Dune context: { name = "default" ; kind = "default" ; profile = User_defined "Release" ; merlin = true ... ; findlib_path = [ External "/home/user/source/diskuv-net-api/build/dev/Release/_opam/lib" ]
Querying Merlin
configuration
has more details. 2. The VS Code OCaml extension queries the default
Opam root ~/.opam
to present to the developer which Opam switches
are available (ie. run env - HOME=$HOME opam switch
). The VS Code
selected Opam switch (which can be saved in ~/.vscode/settings.json
as the "ocaml.sandbox":{"kind": "opam","switch": "..."}
property) is
expected to contain the the ocaml-lsp-server IDE Language
Server.
We provide IDE support by doing the following:
All the
dev
anddev-*
targets (ie. runmake build-dev-Release
) are accessible to VS Code (see point [2] above) by using the default Opam root~/.opam
to register the Opam switches.The
dev
target (an alias to thedev-Debug
which you can run withmake build-dev
ormake build-dev-Debug
) uses the default Dune_build/
subdirectory of the project folder (${workspaceFolder}
in VS Code). This isn’t strictly required for the VS Code OCaml extension but may help other IDEs and other VS Code extensions.We do not define a
./dune-workspace
file containing “(context …)” because doing so would require us to list all valid contexts. That is because if even one “(context …)” is defined thendune build
will ignore the Opam switch in the environment variable OPAMSWITCH we set based on the build type. So we do not define entries like the following:
lisp (context (opam (switch build/dev/Release) (name dev-Release) (merlin) (profile Release) ))
C Code
CMake
CMake is a build tool, primarily for C/C++ cross-platform builds
Much of the best practices and structure come from https://cliutils.gitlab.io/modern-cmake/ and https://gitlab.com/CLIUtils/modern-cmake/tree/master/examples/extended-project.
Visual Studio Code can use the CMake Tools extension.
The build directory is build/TARGET_PLATFORM/BUILD_TYPE
where:
TARGET_PLATFORM
is the name of the kit in.vscode/cmake-kits.json
which corresponds to the target platformBUILD_TYPE}
is the name of the variant like Debug or Release which corresponds to the build type
Note
“Win32” refers to executables that can be installed using a .MSI or a .EXE. More formally they are “PE32/PE32+ executables”. “UWP” is the Universal Windows Platform, which are executables that can be downloaded from the Windows Store. To complicate things further, in 2021 the Windows Store started accepting regular Win32 (not UWP) games in the Windows Store.
For 32 bit Intel/AMD Win32 builds:
$BuildDir = "build\x86-windows-msvc\Debug"
cmake -S . -B $BuildDir -A Win32
cmake --build $BuildDir
For 64 bit Intel/AMD Win32 builds:
$BuildDir = "build\x64-windows-msvc\Debug"
cmake -S . -B $BuildDir -A x64
cmake --build $BuildDir
For 32 bit ARM Win32 builds:
$BuildDir = "build\arm-windows-msvc\Debug"
cmake -S . -B $BuildDir -A arm
cmake --build $BuildDir
For 64 bit ARM Win32 builds:
$BuildDir = "build\arm64-windows-msvc\Debug"
cmake -S . -B $BuildDir -A arm64
cmake --build $BuildDir
Doesn’t produce UWP. For 32 bit Intel/AMD UWP builds:
$BuildDir = "build\x86-uwp-msvc\Debug"
cmake -S . -B $BuildDir -DVCPKG_TARGET_TRIPLET="x86-uwp"
cmake --build $BuildDir
Doesn’t produce UWP. For 64 bit Intel/AMD UWP builds:
$BuildDir = "build\x64-uwp-msvc\Debug"
cmake -S . -B $BuildDir -DVCPKG_TARGET_TRIPLET="x64-uwp"
cmake --build $BuildDir
Doesn’t produce UWP. For 32 bit ARM UWP builds:
$BuildDir = "build\arm-uwp-msvc\Debug"
cmake -S . -B $BuildDir -DVCPKG_TARGET_TRIPLET="arm-uwp"
cmake --build $BuildDir
Doesn’t produce UWP. For 64 bit ARM UWP builds:
$BuildDir = "build\arm64-uwp-msvc\Debug"
cmake -S . -B $BuildDir -DVCPKG_TARGET_TRIPLET="arm64-uwp"
cmake --build $BuildDir
The build systems are defined at
https://github.com/microsoft/vcpkg/tree/master/triplets and
https://github.com/microsoft/vcpkg/tree/master/triplets/community.
Installing is:
cmake --install $BuildDir
vcpkg
vcpkg is a C/C++ package manager (think
pip
for Python orGradle
for Java)
vcpkg is automatically built as
part of the Building steps using the
scripts/setup/PLATFORM/install-tools.(sh|ps1)
script.
There are two ways to install vcpkg packages: classic and manifest mode. We use the newer manifest mode.
You can run vcpkg
with the following on Unix:
./src/build-tools/vendor/vcpkg/vcpkg --version
or the following on Windows:
.\src\build-tools\vendor\vcpkg\vcpkg --version
The vcpkg search
command is useful to find the exact name of a new
package you may install with vcpkg install
and then include the
package in
vcpkg.json
and then include the package in
CMakeLists.txt.
To get updates to existing packages:
Get a newer tag of
src/build-tools/vendor/vcpkg
(ex.cd src/build-tools/vendor/vcpkg; git fetch --tags; git checkout SOME_NEW_TAG
).Run
vcpkg upgrade
to rebuild all outdated packages.
Linux
C Runtime Library
We use the alternative C runtime library musl
for Linux. It is:
can be statically linked. This is extremely important for Linux so we don’t have a nightmare distributing many different executables matching the specific GNU libc and related libraries in Ubuntu18/Ubuntu20/RHEL5/ad infinimum. Static linking is not much of a problem for Windows or macOS since they have stable system C libraries.
liberally licensed
builds on a huge number of target platforms (especially embedded platforms)
avoids glibc incompatibility problems with Qemu (which creates a red herring by complaining about old kernel versions); more details at https://github.com/dockcross/dockcross/issues/274
Hardware Architectures
We can use Qemu to emulate hardware. Emulation is very important so that test code that is created alongside the build is actually executed and validated.
https://dbhi.github.io/qus/ has a like-minded detailed description of
this type of approach. We use the qus
Docker images to register
transparent Qemu userland emulation in the host kernel (Microsoft Linux
Kernel for WSL2; the desktop kernel for Linux; etc.) so that running
something like an ARM compiled hello_arm
will delegate to Qemu for
CPU emulation.
Userland
The userland is the executables and libraries that live outside the
kernel. To make the build process work without cross-compiling, we need
all of the userland including bash
, the C Runtime library and
Node.js to be available in the host architecture or the target
architecture. More importantly when the C compiler generates code it
must think that the architecture is the target architecture so that any
executables we want to distribute are built for the target architecture.
One important consequence is that any static libraries that are included
as part of the distribution executables must be compiled in the target
architecture; the libraries cannot be the host architecture because the
transparent Qemu translation is for executables not libraries.
https://ownyourbits.com/2018/06/13/transparently-running-binaries-from-any-architecture-in-linux-with-qemu-and-binfmt_misc/ has a technique we will use to fetch the entire userland in the target architecture we want.
After implementing the solution, I came across https://github.com/alpinelinux/alpine-chroot-install. It does not do QEMU for various hardware architectures but is a great reference nonetheless. It is especially important to look at if we use GitHub Actions or Travis CI.
So inside the AMD64 Docker container we build a chroot sandbox called the Build Sandbox with a musl-based filesystem from the target architecture.
Build Sandbox
The Build Sandbox is a musl-based chroot sandbox is simply an Alpine distribution which comes with simple instructions to create an architecture specific sandbox.
See the last section for how the Build Sandbox is carved out of the container’s userland.
We add Alpine packages that we need that include the executables:
being able to install new packages (ex.
apk
orapt-get
)bash
andmake
which are required for Opamgcc
/g++
which is required for CMake and OCaml native compilation (ocamlopt)
Opam will need to be configured to not do sandboxing which would fail because nested sandboxes are poorly supported.
Limitations on Hardware Architecture
Be aware that:
Using Alpine as the source for our musl-based chroot sandbox limits our hardware architecture choices to what Alpine officially supports. See http://mirror.csclub.uwaterloo.ca/alpine/latest-stable/releases/ for the list of supported architectures. An alternative would be to use OpenWRT Linux which supports even more architectures, but we stick to Alpine since it has way more packages.
OCaml native code compilation limits choices as well. We could use OCaml bytecode for non-native architectures but we haven’t done that work. The list of supported platforms is at https://ocaml.org/learn/portability.html with releases (like https://ocaml.org/releases/4.12.0.html) listing new platform support.
In practice Alpine is the limiting factor.
C Code
musl
is built locally (this can take hours) by
vendor/musl-cross-make
and configured by
scripts/unix/musl-cross-make.config.mak
. Some of the configuration,
for example, is used to detect that an ARM machine should use the target
triplet arm-linux-muslabihf
to produce correct machine code with
FPU-specific floating point calling
conventions.
make -f scripts/unix/musl-cross-make.config.mak print-TARGET
shows
the detected target triplet. Let’s assume the target triplet is
x86_64-linux-musl
. Then by setting VCPKG_TARGET_TRIPLET
we use
the vcpkg triplet file etc/vcpkg/triplets/x86_64-linux-musl.cmake
to
make sure all vcpkg packages use the locally built musl
compilers
and are statically linked.
Finally, we need our own C code (not the vcpkg packages) to use the
musl
compilers. We use the multiple toolchain files feature of
vcpkg
by setting VCPKG_CHAINLOAD_TOOLCHAIN_FILE
to a musl toolchain in
cmake/toolchains/
.
OCaml
We use an OPAM variant that already includes musl
. In Esy’s
package.json
/esy.json
we can use a resolution like:
{
"resolutions": {
"ocaml": "4.12.0-musl.static.flambda"
}
}
Building
Command Line
BUILDDIR=build/dev/Debug
TARGETTRIPLET=$(make -f scripts/unix/musl-cross-make.config.mak print-TARGET)
PATH="$PWD/vendor/musl-cross-make/output/bin:$PATH"
build/_tools/cmake/bin/cmake -S . -B $BUILDDIR -DVCPKG_TARGET_TRIPLET=$TARGETTRIPLET -DVCPKG_CHAINLOAD_TOOLCHAIN_FILE=$PWD/cmake/toolchains/of-vcpkg-target-triplet.cmake
cmake --build $BUILDDIR
Installing is:
cmake --build $BUILDDIR --target install
Inspecting a build sandbox is (you can change the PLATFORM and BUILDTYPE arguments):
scripts/unix/within-sandbox.sh -p linux_arm64 -b Debug