June 02, 2025 in windows ocaml synthesizer audio
I got my OCaml music synthesizer working on Windows. Here’s all the things that went wrong in the process.
This post will chronicle my attempt to use OCaml to play generated audio samples on a Windows PC. This is part of my work on the llama synthesizer library, where I’ve been rewriting the low-level audio logic formerly written in Rust (with cpal) into OCaml to simplify the build process and development experience. I’ve written down some of my complaints about the developer experience working on llama in a previous post and some of those difficulties came from complications working with Rust/OCaml interop, so removing all Rust from the project will help reduce friction developing it further.
I have a proof-of-concept version of llama based on OCaml’s libao bindings which works on Linux and MacOS. Libao claims to support Windows but I haven’t figured out how to install it on Windows so I might experiment with some easier-to-install alternatives in this post.
The intention of this post is to serve as a guide to anyone getting started with OCaml on Windows, to highlight some of the remaining problems with the OCaml ecosystem on Windows, and to demonstrate some usability issues with Opam in general. My summary is that OCaml on Windows is easier than I expected it to be, but there are still some rough edges that newcomers should be aware of to properly set expectations. I wrote down everything I tried as I tried it, so all the mistakes, blind alleys and failed debugging experiments will be documented here as well as the paths that eventually led to solutions.
Install Opam
For the purposes of this post I’ll set up an entire OCaml environment on Windows from scratch. I’ll be using PowerShell 7.5.1 for all shell commands in this post. For the purposes of translating commands and paths to other machines, I’ll state explicitly that my username is “s”.
We’ll install Opam with WinGet. I’ve had some difficulty setting up WinGet in the past and written about setting it up properly in a previous post but today it worked as expected.
PS C:\Users\s> winget install Git.Git OCaml.opam ... PS C:\Users\s> opam init ... How should opam obtain Unix tools? > 1. Automatically create an internal Cygwin installation that will be managed by opam (recommended) 2. Use an existing Cygwin/MSYS2 installation 3. Abort initialisation [1/2/3] 1 <-- here I selected option 1 ...Allowing Opam to manage its own Cygwin installation is the recommended way of using Opam on Windows, so that’s what I’ll try first. This results in Cygwin being installed to C:\Users\s\AppData\Local\opam\.cygwin. This might come in handy to know if we ever need to manually install a Cygwin package, though hopefully Opam can take care of installing any such packages via Opam’s depexts system which knows how to install system-specific packages needed by Opam packages by directly invoking the package manager to install the appropriately-named package.
Bear in mind that Opam’s initialization can take 15 minutes or so on Windows with most of the time spent building the OCaml compiler. Compiling OCaml code on Windows tends to be much slower than on MacOS and Linux. I don’t understand why.
Install a sound library
We need to choose a library in the Opam repository that can give us access to the sound card. Ideally we would use a library with a depext tailored to Windows with Cygwin, as that way Opam can take care of installing any system dependencies via Cygwin. Since my experimental version of llama is already implemented with libao that would be the most convenient choice, however its depexts don’t look promising:
PS C:\Users\s> opam show conf-ao ... depexts ["libao-devel"] {os-distribution = "centos" | os-family = "fedora" | os-family = "suse" | os-family = "opensuse"} ["libao"] {os = "freebsd" | os = "macos" & os-distribution = "homebrew" | os-distribution = "nixos" | os-family = "arch"} ["libao-dev"] {os-family = "debian" | os-family = "ubuntu" | os-distribution = "alpine"}There are no entries in that list for Windows, so using libao would require setting it up manually which I’d prefer to avoid.
Another candidate library is portaudio.
PS C:\Users\s> opam show conf-portaudio ... depexts ["portaudio-dev"] {os-distribution = "alpine"} ["portaudio-devel"] {os-distribution = "centos" | os-distribution = "fedora" | os-family = "suse" | os-family = "opensuse"} ["portaudio"] {os = "macos" & os-distribution = "homebrew"} ["portaudio"] {os = "freebsd" | os-family = "arch" | os-distribution = "nixos" | os-distribution = "ol"} ["portaudio"] {os = "win32" & os-distribution = "cygwinports"} ["portaudio19-dev"] {os-family = "debian" | os-family = "ubuntu"}That’s more promising as there’s a depext that gets chosen on {os = "win32" & os-distribution = "cygwinports"}. It’s a little concerning that the name of the os-distribution is “cygwinports” and not just “cygwin”. I’ve never come across cygwinports before, but maybe it won’t matter. The next step is to see if installing conf-portaudio with Opam causes the right depext to be installed with Cygwin:
PS C:\Users\s> opam install conf-portaudio The following actions will be performed: === recompile 1 package ↻ mingw-w64-shims 0.2.0 [uses conf-pkg-config] === install 2 packages ∗ conf-pkg-config 4 [required by conf-portaudio] ∗ conf-portaudio 1 Proceed with ↻ 1 recompilation and ∗ 2 installations? [y/n] y The following system packages will first need to be installed: pkgconf <><> Handling external dependencies <><><><><><><><><><><><><><><><><><><><> 🐫 + C:\Users\s\AppData\Local\opam\.cygwin\setup-x86_64.exe "--root" "C:\\Users\\s\\AppData\\Local\\opam\\.cygwin\\root" "--quiet-mode" "noinput" "--no-shortcuts" "--no-startmenu" "--no-desktop" "--no-admin" "--no-version-check" "--no-write-registry" "--packages" "pkgconf" "--upgrade-also" "--only-site" "--site" "https://cygwin.mirror.constant.com/" "--local-package-dir" "C:\\Users\\s\\AppData\\Local\\opam\\.cygwin\\cache" - Starting cygwin install, version 2.934 - User has NO backup/restore rights - User has NO symlink creation right - io_stream_cygfile: fopen(/etc/setup/setup.rc) failed 2 No such file or directory - Current Directory: C:\Users\s\AppData\Local\opam\.cygwin\cache - root: C:\Users\s\AppData\Local\opam\.cygwin\root user - Changing gid back to original - Selected local directory: C:\Users\s\AppData\Local\opam\.cygwin\cache - net: Preconfig - site: https://cygwin.mirror.constant.com/ - solving: 1 tasks, update: yes, use test packages: no - solving: 2 tasks, update: no, use test packages: no - Augmented Transaction List: - 0 install libpkgconf6 2.4.3-1 - 1 install pkgconf 2.4.3-1 - Downloaded C:\Users\s\AppData\Local\opam\.cygwin\cache/https%3a%2f%2fcygwin.mirror.constant.com%2f/x86_64/release/pkgconf/libpkgconf6/libpkgconf6-2.4.3-1.tar.zst - Downloaded C:\Users\s\AppData\Local\opam\.cygwin\cache/https%3a%2f%2fcygwin.mirror.constant.com%2f/x86_64/release/pkgconf/pkgconf-2.4.3-1.tar.zst - Extracting from file://C:\Users\s\AppData\Local\opam\.cygwin\cache/https%3a%2f%2fcygwin.mirror.constant.com%2f/x86_64/release/pkgconf/libpkgconf6/libpkgconf6-2.4.3-1.tar.zst - Extracting from file://C:\Users\s\AppData\Local\opam\.cygwin\cache/https%3a%2f%2fcygwin.mirror.constant.com%2f/x86_64/release/pkgconf/pkgconf-2.4.3-1.tar.zst - running: C:\Users\s\AppData\Local\opam\.cygwin\root\bin\dash.exe "/etc/postinstall/0p_000_autorebase.dash" - running: C:\Users\s\AppData\Local\opam\.cygwin\root\bin\dash.exe "/etc/postinstall/0p_update-info-dir.dash" - running: C:\Users\s\AppData\Local\opam\.cygwin\root\bin\dash.exe "/etc/postinstall/zp_man-db-update-index.dash" - Ending cygwin install <><> Processing actions <><><><><><><><><><><><><><><><><><><><><><><><><><> 🐫 ⬇ retrieved mingw-w64-shims.0.2.0 (cached) ⊘ removed mingw-w64-shims.0.2.0 ∗ installed conf-pkg-config.4 #=== ERROR while compiling conf-portaudio.1 ===================================# "pkg-config": command not found. <><> Error report <><><><><><><><><><><><><><><><><><><><><><><><><><><><><> 🐫 ┌─ The following actions failed │ λ build conf-portaudio 1 └─ ┌─ The following changes have been performed (the rest was aborted) │ ⊘ remove mingw-w64-shims 0.2.0 │ ∗ install conf-pkg-config 4 └─A few things have gone wrong here. Firstly, from:
The following system packages will first need to be installed: pkgconf…we can see that Opam has detected that some depexts need to be installed, but only pkgconf and not portaudio are on the list. To help understand why, look at the depexts of the conf-pkg-config package:
PS C:\Users\s> opam show conf-pkg-config ... depexts ["pkg-config"] {os-family = "debian" | os-family = "ubuntu"} ... ["system:pkgconf"] {os = "win32" & os-distribution = "cygwinports"} ["pkgconf"] {os-distribution = "cygwin"}This package makes an explicit distinction between “cygwinports” and “cygwin” so it stands to reason that my initial concerns about os-distribution = "cygwinports" were merited.
The second issue is that even though Opam installed pkgconf with Cygwin, the pkg-config executable couldn’t be found while installing the conf-portaudio package. Remember earlier we located the Cygwin installation that Opam will be managing? Looking in C:\Users\s\AppData\Local\opam\.cygwin\root\bin, there is a program pkgconf.exe, but not a pkg-config.exe. At some point in the past few years pkg-config was renamed to pkgconf and this change is slowly trickling into different package managers at different rates.
The first thing to try here is modifying the package metadata for conf-portaudio to use the correct name for pkgconf. Start by saving the existing metadata to a local file which we can then modify:
PS C:\Users\s> opam show conf-portaudio --raw > conf-portaudio.opamWe should now be able to install the conf-portaudio package from this file rather than using the metadata stored in the Opam repo by running:
PS C:\Users\s> opam install .\conf-portaudio.opamThis command hung with Processing 1/1: [conf-portaudio.1: rsync] so I left it running while I went to run some errands and when I got back over an hour later it was still hung.
I cancelled the hung operation. Despite not completing it did have the side effect of creating an Opam “pin” for the conf-portaudio package. An Opam pin is a configuration to override the source of a package, usually to allow for local development of said package. We can see all the current Opam pins with:
PS C:\Users\s> opam pin list conf-portaudio.1 (uninstalled) rsync file://C:/Users/sBefore proceeding I wanted to remove the pin since something has clearly gone wrong:
PS C:\Users\s> opam pin remove conf-portaudio Cannot remove C:\Users\s\AppData\Local\opam\default\.opam-switch\sources\conf-portaudio\ AppData\Local\Application Data\Application Data\Application Data\Application Data\ Application Data\Application Data\Application Data\Application Data\Application Data\ Temporary Internet Files (C:\Users\s\AppData\Local\Microsoft\WinGet\Packages\ OCaml.opam_Microsoft.Winget.Source_8wekyb3d8bbwe\opam.exe: "unlink" failed on C:\Users\s\AppData\Local\opam\default\.opam-switch\sources\conf-portaudio\AppData\ Local\Application Data\Application Data\Application Data\Application Data\ Application Data\Application Data\Application Data\Application Data\Application Data\ Temporary Internet Files: No such file or directory).Hmm. The Application Data\Application Data\Application Data component of the path looks suspicious. Opam creates a copy of pinned packages so let’s take a look at conf-portaudio’s copy in C:\Users\s\AppData\Local\opam\default\.opam-switch\sources\conf-portaudio:
PS C:\Users\s> ls .\AppData\Local\opam\default\.opam-switch\sources\conf-portaudio\ Directory: C:\Users\s\AppData\Local\opam\default\.opam-switch\sources\conf-portaudio Mode LastWriteTime Length Name ---- ------------- ------ ---- d---- 04/11/2024 21:49 .bin d---- 04/11/2024 22:26 .cargo d---- 04/11/2024 21:49 .completions d---- 04/11/2024 21:49 .config d---- 22/11/2024 14:34 .dotfiles d---- 04/11/2024 21:49 .emacs.d d---- 25/11/2024 15:12 .local d---- 04/11/2024 21:59 .ms-ad d---- 04/11/2024 22:25 .rustup d---- 04/11/2024 21:29 .ssh d---- 18/11/2024 14:35 .vscode d---- 29/05/2025 16:03 AppData -a--- 28/03/2025 17:00 3654 .bash_history -a--- 25/11/2024 18:07 9168 .bashrc ...Hey, that looks like my home directory! It appears I unwittingly pinned the conf-portaudio package to my entire home directory, and since Opam installs packages inside my home directory it was recurring forever, continuously copying my home directory inside itself. No wonder it hung.
Manually fixing the problem:
PS C:\Users\s> rm .\AppData\Local\opam\default\.opam-switch\sources\conf-portaudio Confirm The item at C:\Users\s\AppData\Local\opam\default\.opam-switch\sources\conf-portaudio has children and the Recurse parameter was not specified. If you continue, all children will be removed with the item. Are you sure you want to continue? [Y] Yes [A] Yes to All [N] No [L] No to All [S] Suspend [?] Help (default is "Y"): A Removed 32536 of 134845 files [1.197 GB of 15.926 GB (48.6 MB/s)That’s almost 16GB of recursive copies of my home directory! This command ended up failing due to permission problems so I had to delete it from the explorer UI instead as I’m not very familiar with file management within PowerShell.
Now that I manually deleted that folder, removing the pin works:
PS C:\Users\s> opam pin remove conf-portaudio Ok, conf-portaudio is no longer pinned to file://C:/Users/s (version 1)What I’ve learnt here is that Opam doesn’t pin individual files but rather entire directories. A pin is a mapping from package to directory, and when you run opam pin .\foo.opam Opam learns the package name is foo from the name of the file (I think), but assumes the directory is the directory containing foo.opam. Most Opam package source directories contain an opam file and the source code for the package. What’s unusual in this case is that the conf-portaudio package does not have source code. We say it’s a “metapackage” as installing it just installs its dependencies (including external dependencies) and runs a command to verify their installation, but it has no source code of its own. So in order to pin conf-portaudio we need to first create a new directory and move the opam file there.
Pinning the directory is sufficient as Opam will scan the directory for any opam files and create pins for each corresponding package using the current directory as the source. I’ve also modified the opam file to run pkgconf rather than pkg-config.
PS C:\Users\s\conf-portaudio> opam pin . [NOTE] Package conf-portaudio is already pinned to file://C:/Users/s/conf-portaudio (version 1). conf-portaudio is now pinned to file://C:/Users/s/conf-portaudio (version 1) The following actions will be performed: === install 1 package ∗ conf-portaudio 1 (pinned) Proceed with ∗ 1 installation? [y/n] n (don't want to install it just yet) [NOTE] Pinning command successful, but your installed packages may be out of sync.By default opam pin will prompt to install the pinned package in addition to just storing the package → directory mapping, but I just want to create the mapping for now so I chose n at the prompt. Before installing the package I want to make sure that pinning it had the desired effect. Ask Opam what it thinks should be the package metadata for conf-portaudio now:
PS C:\Users\s\conf-portaudio> opam show conf-portaudio --raw opam-version: "2.0" name: "conf-portaudio" version: "1" synopsis: "Virtual package relying on portaudio" description: "This package can only install if the portaudio library is installed on the system." maintainer: "https://github.com/ocaml/opam-repository/issues" authors: "portaudio dev team" license: "BSD-1-Clause" homepage: "http://www.portaudio.com/" bug-reports: "https://github.com/ocaml/opam-repository/issues" depends: [ "conf-pkg-config" {build} ] flags: conf build: ["pkgconf" "--exists" "portaudio-2.0"] depexts: [ ["portaudio-dev"] {os-distribution = "alpine"} ["portaudio-devel"] {os-distribution = "centos" | os-family = "fedora" | os-family = "suse" | os-family = "opensuse"} ["portaudio"] {os = "macos" & os-distribution = "homebrew"} ["portaudio"] {os = "freebsd" | os-family = "arch" | os-distribution = "nixos" | os-distribution = "ol"} ["portaudio"] {os = "win32" & os-distribution = "cygwinports"} ["portaudio19-dev"] {os-family = "debian" | os-family = "ubuntu"} ] url { src: "file://C:/Users/s/conf-portaudio" }Note that the build command calls pkgconf rather than pkg-config as it did previously, indicating that my custom version of conf-portaudio is being used.
Now we can try installing it again.
PS C:\Users\s\conf-portaudio> opam install conf-portaudio <><> Synchronising pinned packages ><><><><><><><><><><><><><><><><><><><><> 🐫 [conf-portaudio.1] synchronised (no changes) The following actions will be performed: === install 1 package ∗ conf-portaudio 1 (pinned) <><> Processing actions <><><><><><><><><><><><><><><><><><><><><><><><><><> 🐫 [ERROR] The compilation of conf-portaudio.1 failed at "pkgconf --exists portaudio-2.0". #=== ERROR while compiling conf-portaudio.1 ===================================# # context 2.3.0 | win32/x86_64 | ocaml.5.3.0 | pinned(file://C:/Users/s/conf-portaudio) # path ~\AppData\Local\opam\default\.opam-switch\build\conf-portaudio.1 # command ~\AppData\Local\opam\.cygwin\root\bin\pkgconf.exe --exists portaudio-2.0 # exit-code 1 # env-file ~\AppData\Local\opam\log\conf-portaudio-12212-533065.env # output-file ~\AppData\Local\opam\log\conf-portaudio-12212-533065.out <><> Error report <><><><><><><><><><><><><><><><><><><><><><><><><><><><><> 🐫 ┌─ The following actions failed │ λ build conf-portaudio 1 └─ ╶─ No changes have been performedThat’s progress! There’s now an error message indicating that pkgconf --exists portaudio-2.0 failed, which is expected because the conf-portaudio package’s depexts don’t contain a case for os-distribution = "cygwin" (only os-distribution = "cygwinports").
Next step is to fix the conf-portaudio depexts so they install the appropriate package on Cygwin. I added this line to the depexts section in conf-portaudio.opam:
["portaudio"] {os = "win32" & os-distribution = "cygwin"}I needed to run opam pin . again to get Opam to update its copy of the custom version of this package’s metadata. Every time you change the opam file of a pinned package you have to re-pin the package for the change to be come visible to Opam.
Then I tried installing conf-portaudio again:
PS C:\Users\s\conf-portaudio> opam install conf-portaudio <><> Synchronising pinned packages ><><><><><><><><><><><><><><><><><><><><> 🐫 [conf-portaudio.1] synchronised (no changes) The following actions will be performed: === install 1 package ∗ conf-portaudio 1 (pinned) The following system packages will first need to be installed: portaudio <><> Handling external dependencies <><><><><><><><><><><><><><><><><><><><> 🐫 + C:\Users\s\AppData\Local\opam\.cygwin\setup-x86_64.exe "--root" "C:\\Users\\s\\AppData\\Local\\opam\\.cygwin\\root" "--quiet-mode" "noinput" "--no-shortcuts" "--no-startmenu" "--no-desktop" "--no-admin" "--no-version-check" "--no-write-registry" "--packages" "portaudio" "--upgrade-also" "--only-site" "--site" "https://cygwin.mirror.constant.com/" "--local-package-dir" "C:\\Users\\s\\AppData\\Local\\opam\\.cygwin\\cache" - Starting cygwin install, version 2.934 - User has NO backup/restore rights - User has NO symlink creation right - io_stream_cygfile: fopen(/etc/setup/setup.rc) failed 2 No such file or directory - Current Directory: C:\Users\s\AppData\Local\opam\.cygwin\cache - root: C:\Users\s\AppData\Local\opam\.cygwin\root user - Changing gid back to original - Selected local directory: C:\Users\s\AppData\Local\opam\.cygwin\cache - net: Preconfig - site: https://cygwin.mirror.constant.com/ - Package 'portaudio' not found. - solving: 0 tasks, update: yes, use test packages: no - solving: 0 tasks, update: no, use test packages: no - Augmented Transaction List: is empty - running: C:\Users\s\AppData\Local\opam\.cygwin\root\bin\dash.exe "/etc/postinstall/0p_000_autorebase.dash" - running: C:\Users\s\AppData\Local\opam\.cygwin\root\bin\dash.exe "/etc/postinstall/0p_update-info-dir.dash" - running: C:\Users\s\AppData\Local\opam\.cygwin\root\bin\dash.exe "/etc/postinstall/zp_man-db-update-index.dash" - Ending cygwin install <><> Processing actions <><><><><><><><><><><><><><><><><><><><><><><><><><> 🐫 [ERROR] The compilation of conf-portaudio.1 failed at "pkgconf --exists portaudio-2.0". #=== ERROR while compiling conf-portaudio.1 ===================================# # context 2.3.0 | win32/x86_64 | ocaml.5.3.0 | pinned(file://C:/Users/s/conf-portaudio) # path ~\AppData\Local\opam\default\.opam-switch\build\conf-portaudio.1 # command ~\AppData\Local\opam\.cygwin\root\bin\pkgconf.exe --exists portaudio-2.0 # exit-code 1 # env-file ~\AppData\Local\opam\log\conf-portaudio-9396-6b5019.env # output-file ~\AppData\Local\opam\log\conf-portaudio-9396-6b5019.out <><> Error report <><><><><><><><><><><><><><><><><><><><><><><><><><><><><> 🐫 ┌─ The following actions failed │ λ build conf-portaudio 1 └─ ╶─ No changes have been performedThis time Opam tried to install the portaudio Cygwin package, however Package 'portaudio' not found. tells us that there is no such Cygwin package.
However digging around in the Cygwin package repo I did come across a package named mingw64-x86_64-portaudio, though it’s unmaintained and hasn’t been updated since 2018. I also happened to find the package mingw64-x86_64-libao, also unmaintained and not updated since 2013, however if this package works then I might be able to use llama on Windows unmodified, since it’s currently based on libao.
Repeating the steps above to create a custom version of the conf-ao package - Opam’s meta package for installing the system-appropriate libao package:
PS C:\Users\s\conf-ao> opam show conf-ao --raw > conf-ao.opam PS C:\Users\s\conf-ao> opam pin . conf-ao is now pinned to file://C:/Users/s/conf-ao (version 1) The following actions will be performed: === install 1 package ∗ conf-ao 1 (pinned) Proceed with ∗ 1 installation? [y/n] n [NOTE] Pinning command successful, but your installed packages may be out of sync.Then I modified conf-ao.opam, adding the following to its depexts:
["mingw64-x86_64-libao"] {os = "win32" & os-distribution = "cygwin"}…and changed its build command to test for the system libao package using pkgconf instead of pkg-config, the same as what I did for conf-portaudio.
Moment of truth:
PS C:\Users\s\conf-ao> opam install conf-ao <><> Synchronising pinned packages ><><><><><><><><><><><><><><><><><><><><> 🐫 [conf-ao.1] synchronised (no changes) The following actions will be performed: === install 1 package ∗ conf-ao 1 (pinned) The following system packages will first need to be installed: mingw64-x86_64-libao <><> Handling external dependencies <><><><><><><><><><><><><><><><><><><><> 🐫 + C:\Users\s\AppData\Local\opam\.cygwin\setup-x86_64.exe "--root" "C:\\Users\\s\\AppData\\Local\\opam\\.cygwin\\root" "--quiet-mode" "noinput" "--no-shortcuts" "--no-startmenu" "--no-desktop" "--no-admin" "--no-version-check" "--no-write-registry" "--packages" "mingw64-x86_64-libao" "--upgrade-also" "--only-site" "--site" "https://cygwin.mirror.constant.com/" "--local-package-dir" "C:\\Users\\s\\AppData\\Local\\opam\\.cygwin\\cache" - Starting cygwin install, version 2.934 - User has NO backup/restore rights - User has NO symlink creation right - Current Directory: C:\Users\s\AppData\Local\opam\.cygwin\cache - root: C:\Users\s\AppData\Local\opam\.cygwin\root user - Changing gid back to original - Selected local directory: C:\Users\s\AppData\Local\opam\.cygwin\cache - net: Preconfig - site: https://cygwin.mirror.constant.com/ - solving: 1 tasks, update: yes, use test packages: no - solving: 1 tasks, update: no, use test packages: no - Augmented Transaction List: - 0 install mingw64-x86_64-libao 1.1.0-1 - Downloaded C:\Users\s\AppData\Local\opam\.cygwin\cache/https%3a%2f%2fcygwin.mirror.constant.com%2f/noarch/release/mingw64-x86_64-libao/mingw64-x86_64-libao-1.1.0-1.tar.xz - Extracting from file://C:\Users\s\AppData\Local\opam\.cygwin\cache/https%3a%2f%2fcygwin.mirror.constant.com%2f/noarch/release/mingw64-x86_64-libao/mingw64-x86_64-libao-1.1.0-1.tar.xz - running: C:\Users\s\AppData\Local\opam\.cygwin\root\bin\dash.exe "/etc/postinstall/0p_000_autorebase.dash" - running: C:\Users\s\AppData\Local\opam\.cygwin\root\bin\dash.exe "/etc/postinstall/0p_update-info-dir.dash" - running: C:\Users\s\AppData\Local\opam\.cygwin\root\bin\dash.exe "/etc/postinstall/zp_man-db-update-index.dash" - Ending cygwin install <><> Processing actions <><><><><><><><><><><><><><><><><><><><><><><><><><> 🐫 [ERROR] The compilation of conf-ao.1 failed at "pkgconf --exists ao". #=== ERROR while compiling conf-ao.1 ==========================================# # context 2.3.0 | win32/x86_64 | ocaml.5.3.0 | pinned(file://C:/Users/s/conf-ao) # path ~\AppData\Local\opam\default\.opam-switch\build\conf-ao.1 # command ~\AppData\Local\opam\.cygwin\root\bin\pkgconf.exe --exists ao # exit-code 1 # env-file ~\AppData\Local\opam\log\conf-ao-8080-676a1b.env # output-file ~\AppData\Local\opam\log\conf-ao-8080-676a1b.out <><> Error report <><><><><><><><><><><><><><><><><><><><><><><><><><><><><> 🐫 ┌─ The following actions failed │ λ build conf-ao 1 └─ ╶─ No changes have been performedOk so it found the mingw64-x86_64-libao package in the Cygwin repo but it looks like pkgconf can’t see it.
Manually inspecting the Cygwin directory, and I do see that libao has been installed:
PS C:\Users\s\conf-ao> ls C:\Users\s\AppData\Local\opam\.cygwin\root\usr\x86_64-w64-mingw32\sys-root\mingw\lib\libao.* Directory: C:\Users\s\AppData\Local\opam\.cygwin\root\usr\x86_64-w64-mingw32\sys-root\mingw\lib Mode LastWriteTime Length Name ---- ------------- ------ ---- -a--- 02/12/2013 15:27 50102 libao.a -a--- 02/12/2013 15:27 9386 libao.dll.aBut that’s doesn’t look like a standard path for libraries. Reading more about the mingw64-x86_64-* packages in the Cygwin repo and it doesn’t look like I’ll easily be able to mix them with other Cygwin packages, which could make it tricky to proceed with Opam in its current configuration where it manages a Cygwin environment for us.
An alternative package manager for Windows is “Msys2”. During opam init you can select Msys2 as an alternative to Cygwin, though there’s no option for Opam to install the Msys2 environment for us. There appears to be a libao package in the Msys2 repository, so the next step will be to try setting up an Msys2 environment and a new Opam installation which uses it and then install the conf-ao package along with its depexts within that environment.
Trying again with Msys2
Firstly I deleted C:\Users\s\AppData\Local\opam to get a fresh start.
I installed Msys2 from the installer at its website, which created a directory C:\msys64 which will be the root directory of our Msys2 environment.
Then I reran opam init to create a new Opam environment, this time selecting the second option for obtaining Unix tools: Use an existing Cygwin/MSYS2 installation. I’ll post the entire output of the interactive session with opam init in case it helps someone set up Opam on their machine:
PS C:\Users\s> opam init No configuration file found, using built-in defaults. <><> Windows Developer Mode <><><><><><><><><><><><><><><><><><><><><><><><> 🐫 opam does not require Developer Mode to be enabled on Windows, but it is recommended, in particular because it enables support for symlinks without requiring opam to be run elevated (which we do not recommend doing). More information on enabling Developer Mode may be obtained from https://learn.microsoft.com/en-gb/windows/apps/get-started/enable-your-device-for-development <><> Unix support infrastructure ><><><><><><><><><><><><><><><><><><><><><> 🐫 opam and the OCaml ecosystem in general require various Unix tools in order to operate correctly. At present, this requires the installation of Cygwin to provide these tools. How should opam obtain Unix tools? > 1. Automatically create an internal Cygwin installation that will be managed by opam (recommended) 2. Use an existing Cygwin/MSYS2 installation 3. Abort initialisation [1/2/3] 2 Enter the prefix of an existing Cygwin installation (e.g. C:\cygwin64) C:\msys64 The following system packages will first need to be installed: diffutils make patch rsync unzip <><> Handling external dependencies <><><><><><><><><><><><><><><><><><><><> 🐫 opam believes some required external dependencies are missing. opam can: > 1. Run C:\msys64\usr\bin\pacman.exe to install them (may need root/sudo access) 2. Display the recommended C:\msys64\usr\bin\pacman.exe command and wait while you run it manually (e.g. in another terminal) 3. Continue anyway, and, upon success, permanently register that this external dependency is present, but not detectable 4. Abort the installation [1/2/3/4] 1 + C:\msys64\usr\bin\pacman.exe "-Su" "--noconfirm" "diffutils" "make" "patch" "rsync" "unzip" - :: Starting core system upgrade... - there is nothing to do - :: Starting full system upgrade... - resolving dependencies... - looking for conflicting packages... - - Packages (6) libxxhash-0.8.3-1 diffutils-3.10-1 make-4.4.1-2 patch-2.7.6-3 rsync-3.4.1-1 unzip-6.0-3 - - Total Download Size: 1.43 MiB - Total Installed Size: 4.51 MiB - - :: Proceed with installation? [Y/n] - :: Retrieving packages... - make-4.4.1-2-x86_64 downloading... - diffutils-3.10-1-x86_64 downloading... - rsync-3.4.1-1-x86_64 downloading... - unzip-6.0-3-x86_64 downloading... - patch-2.7.6-3-x86_64 downloading... - libxxhash-0.8.3-1-x86_64 downloading... - checking keyring... - checking package integrity... - loading package files... - checking for file conflicts... - checking available disk space... - :: Processing package changes... - installing diffutils... - installing make... - installing patch... - Optional dependencies for patch - ed: for patch -e functionality - installing libxxhash... - installing rsync... - installing unzip... - :: Running post-transaction hooks... - (1/1) Updating the info directory file... Checking for available remotes: rsync and local, git. - you won't be able to use mercurial repositories unless you install the hg command on your system. - you won't be able to use darcs repositories unless you install the darcs command on your system. <><> Fetching repository information ><><><><><><><><><><><><><><><><><><><> 🐫 [default] Initialised <><> Required setup - please read <><><><><><><><><><><><><><><><><><><><><> 🐫 In normal operation, opam only alters files within ~\AppData\Local\opam. However, to best integrate with your system, some environment variables should be set. When you want to access your opam installation, you will need to run: (& opam env) -split '\r?\n' | ForEach-Object { Invoke-Expression $_ } You can always re-run this setup with 'opam init' later. opam doesn't have any configuration options for pwsh; you will have to run (& opam env) -split '\r?\n' | ForEach-Object { Invoke-Expression $_ } whenever you change you current 'opam switch' or start a new terminal session. Alternatively, would you like to select a different shell? [y/n] n <><> Creating initial switch 'default' (invariant ["ocaml" {>= "4.05.0"}] - initially with ocaml-base-compiler) <><> Installing new switch packages <><><><><><><><><><><><><><><><><><><><> 🐫 Switch invariant: ["ocaml" {>= "4.05.0"}] The following system packages will first need to be installed: mingw-w64-x86_64-gcc <><> Handling external dependencies <><><><><><><><><><><><><><><><><><><><> 🐫 opam believes some required external dependencies are missing. opam can: > 1. Run C:\msys64\usr\bin\pacman.exe to install them (may need root/sudo access) 2. Display the recommended C:\msys64\usr\bin\pacman.exe command and wait while you run it manually (e.g. in another terminal) 3. Continue anyway, and, upon success, permanently register that this external dependency is present, but not detectable 4. Abort the installation [1/2/3/4] 1 + C:\msys64\usr\bin\pacman.exe "-Su" "--noconfirm" "mingw-w64-x86_64-gcc" - :: Starting core system upgrade... - there is nothing to do - :: Starting full system upgrade... - resolving dependencies... - looking for conflicting packages... - - Packages (16) mingw-w64-x86_64-binutils-2.44-1 mingw-w64-x86_64-crt-git-12.0.0.r509.g079e6092b-1 mingw-w64-x86_64-gcc-libs-14.2.0-2 mingw-w64-x86_64-gettext-runtime-0.23.1-1 mingw-w64-x86_64-gmp-6.3.0-2 mingw-w64-x86_64-headers-git-12.0.0.r509.g079e6092b-1 mingw-w64-x86_64-isl-0.27-1 mingw-w64-x86_64-libiconv-1.18-1 mingw-w64-x86_64-libwinpthread-git-12.0.0.r509.g079e6092b-1 mingw-w64-x86_64-mpc-1.3.1-2 mingw-w64-x86_64-mpfr-4.2.1-2 mingw-w64-x86_64-windows-default-manifest-6.4-4 mingw-w64-x86_64-winpthreads-git-12.0.0.r509.g079e6092b-1 mingw-w64-x86_64-zlib-1.3.1-1 mingw-w64-x86_64-zstd-1.5.7-1 mingw-w64-x86_64-gcc-14.2.0-2 - - Total Download Size: 65.78 MiB - Total Installed Size: 518.69 MiB - - :: Proceed with installation? [Y/n] - :: Retrieving packages... - mingw-w64-x86_64-gcc-14.2.0-2-any downloading... - mingw-w64-x86_64-headers-git-12.0.0.r509.g079e6092b-1-any downloading... - mingw-w64-x86_64-binutils-2.44-1-any downloading... - mingw-w64-x86_64-crt-git-12.0.0.r509.g079e6092b-1-any downloading... - mingw-w64-x86_64-isl-0.27-1-any downloading... - mingw-w64-x86_64-gcc-libs-14.2.0-2-any downloading... - mingw-w64-x86_64-libiconv-1.18-1-any downloading... - mingw-w64-x86_64-zstd-1.5.7-1-any downloading... - mingw-w64-x86_64-gmp-6.3.0-2-any downloading... - mingw-w64-x86_64-mpfr-4.2.1-2-any downloading... - mingw-w64-x86_64-gettext-runtime-0.23.1-1-any downloading... - mingw-w64-x86_64-mpc-1.3.1-2-any downloading... - mingw-w64-x86_64-zlib-1.3.1-1-any downloading... - mingw-w64-x86_64-winpthreads-git-12.0.0.r509.g079e6092b-1-any downloading... - mingw-w64-x86_64-libwinpthread-git-12.0.0.r509.g079e6092b-1-any downloading... - mingw-w64-x86_64-windows-default-manifest-6.4-4-any downloading... - checking keyring... - checking package integrity... - loading package files... - checking for file conflicts... - checking available disk space... - :: Processing package changes... - installing mingw-w64-x86_64-libwinpthread-git... - installing mingw-w64-x86_64-gcc-libs... - installing mingw-w64-x86_64-libiconv... - installing mingw-w64-x86_64-gettext-runtime... - installing mingw-w64-x86_64-zlib... - installing mingw-w64-x86_64-zstd... - installing mingw-w64-x86_64-binutils... - installing mingw-w64-x86_64-headers-git... - installing mingw-w64-x86_64-crt-git... - installing mingw-w64-x86_64-gmp... - installing mingw-w64-x86_64-isl... - installing mingw-w64-x86_64-mpfr... - installing mingw-w64-x86_64-mpc... - installing mingw-w64-x86_64-windows-default-manifest... - installing mingw-w64-x86_64-winpthreads-git... - installing mingw-w64-x86_64-gcc... <><> Processing actions <><><><><><><><><><><><><><><><><><><><><><><><><><> 🐫 ∗ installed arch-x86_64.1 ∗ installed base-bigarray.base ∗ installed base-threads.base ∗ installed base-unix.base ∗ installed host-arch-x86_64.1 ∗ installed host-system-mingw.1 ∗ installed msys2-mingw64.1 ⬇ retrieved msys2.0.1.0 (https://opam.ocaml.org/cache) ∗ installed msys2.0.1.0 ∗ installed conf-mingw-w64-gcc-x86_64.1 ⬇ retrieved flexdll.0.44 (https://opam.ocaml.org/cache) ∗ installed flexdll.0.44 ∗ installed ocaml-env-mingw64.1 ∗ installed ocaml-options-vanilla.1 ∗ installed system-mingw.1 ⬇ retrieved ocaml-config.3 (2 extra sources) ⬇ retrieved ocaml-config.3 (2 extra sources) ⬇ retrieved ocaml-compiler.5.3.0 (https://opam.ocaml.org/cache) ∗ installed ocaml-compiler.5.3.0 ∗ installed ocaml-base-compiler.5.3.0 ∗ installed ocaml-config.3 ∗ installed ocaml.5.3.0 ∗ installed base-domains.base ∗ installed base-effects.base ∗ installed base-nnp.base Done. # To update the current shell environment, run: (& opam env --switch=default) -split '\r?\n' | ForEach-Object { Invoke-Expression $_ }During initialization, Opam took care of installing a C compiler and various other essential tools into the Msys2 environment at C:\msys64.
My next goal is to get Opam to install libao with Msys2. I’m not sure what the correct depexts entry should be for this, so I’ll start with ["libao"] {os = "win32" & os-distribution = "cygwin"}. I updated my custom copy of the conf-ao package and pinned the package with Opam:
PS C:\Users\s\conf-ao> opam pin .\conf-ao.opam Fatal error: "rsync": command not found.I guess I need to manually install rsync into my Msys2 environment? To do this, launch an Msys2 terminal by running the msys2.exe program from the Msys2 installation (C:\msys64\msys2.exe). Msys2 uses pacman as its package manager, borrowed from Archlinux:
$ pacman -S rsync warning: rsync-3.4.1-1 is up to date -- reinstalling resolving dependencies... looking for conflicting packages... Packages (1) rsync-3.4.1-1 Total Installed Size: 0.67 MiB Net Upgrade Size: 0.00 MiB :: Proceed with installation? [Y/n] nOk looks like rsync was already installed.
Maybe when Opam isn’t managing Cygwin for us we need to to manually update the shell environment to bring various commands into PATH?
PS C:\Users\s\conf-ao> (& opam env --switch=default) -split '\r?\n' | ForEach-Object { Invoke-Expression $_ } PS C:\Users\s\conf-ao> opam pin .\conf-ao.opam Fatal error: "rsync": command not found.No such luck.
Checking that rsync works as expected:
PS C:\Users\s\conf-ao> C:\msys64\usr\bin\rsync --help rsync version 3.4.1 protocol version 32 Copyright (C) 1996-2025 by Andrew Tridgell, Wayne Davison, and others. Web site: https://rsync.samba.org/ ...Maybe I just need to manually add C:\msys64\usr\bin to my PATH.
PS C:\Users\s\conf-ao> $env:PATH += ";C:\msys64\usr\bin" PS C:\Users\s\conf-ao> rsync --version rsync version 3.4.1 protocol version 32 Copyright (C) 1996-2025 by Andrew Tridgell, Wayne Davison, and others. Web site: https://rsync.samba.org/ ...And now pinning conf-ao again:
PS C:\Users\s\conf-ao> opam pin .\conf-ao.opam [ERROR] Could not retrieve Could not extract archive: Unknown archive type: C:\Users\s\AppData\Local\Temp\opam-4184-d29c4e\conf-ao.opamWhich reminds me that I’m not supposed to pin the opam file directly but instead the entire folder which contains it.
PS C:\Users\s\conf-ao> opam pin . conf-ao is now pinned to file://C:/Users/s/conf-ao (version 1) The following actions will be performed: === install 3 packages ∗ conf-ao 1 (pinned) ∗ conf-mingw-w64-pkgconf-x86_64 1 [required by conf-pkg-config] ∗ conf-pkg-config 4 [required by conf-ao] Proceed with ∗ 3 installations? [y/n] n [NOTE] Pinning command successful, but your installed packages may be out of sync.That’s good. I’m curious about the conf-mingw-w64-pkgconf-x86_64 package. It wasn’t needed when installing depexts with Cygwin, so it’s probably specific to Msys2, which means it can probably give us a hint as to the proper way to specify Msys2-specific depexts, which we’ll need to do for our pinned conf-ao package (I was only guessing how to add its new depexts initially).
conf-mingw-w64-pkgconf-x86_64 is probably a dependency of conf-pkg-config on Msys2, so I’ll start by looking at conf-pkg-config’s metadata and then look at the metadata for conf-mingw-w64-pkgconf-x86_64.
PS C:\Users\s\conf-ao> opam show --raw conf-pkg-config ... depends: [ ("host-arch-x86_64" {os = "win32" & os-distribution = "msys2"} & "conf-mingw-w64-pkgconf-x86_64" {os = "win32" & os-distribution = "msys2"} | "host-arch-x86_32" {os = "win32" & os-distribution = "msys2"} & "conf-mingw-w64-pkgconf-i686" {os = "win32" & os-distribution = "msys2"}) ] ... PS C:\Users\s\conf-ao> opam show --raw conf-mingw-w64-pkgconf-x86_64 ... depexts: ["mingw-w64-x86_64-pkgconf"] {os = "win32" & os-distribution = "msys2"}From this we can learn that when using Msys2 to install depexts, the os-distribution variable evaluates to “msys2” instead of “cygwin”. Also on Msys2 Opam will install the system package mingw-w64-x86_64-pkgconf to install pkg-config/pkgconf. This suggests that the correct depext entry for conf-ao is in fact:
["mingw-w64-x86_64-libao"] {os = "win32" & os-distribution = "msys2"}After updating the local copy of conf-ao with the above depext and re-pinning it I tried installing conf-ao again:
PS C:\Users\s\conf-ao> opam install conf-ao <><> Synchronising pinned packages ><><><><><><><><><><><><><><><><><><><><> 🐫 [conf-ao.1] synchronised (no changes) The following actions will be performed: === install 3 packages ∗ conf-ao 1 (pinned) ∗ conf-mingw-w64-pkgconf-x86_64 1 [required by conf-pkg-config] ∗ conf-pkg-config 4 [required by conf-ao] Proceed with ∗ 3 installations? [y/n] y The following system packages will first need to be installed: mingw-w64-x86_64-libao mingw-w64-x86_64-pkgconf <><> Handling external dependencies <><><><><><><><><><><><><><><><><><><><> 🐫 opam believes some required external dependencies are missing. opam can: > 1. Run C:\msys64\usr\bin\pacman.exe to install them (may need root/sudo access) 2. Display the recommended C:\msys64\usr\bin\pacman.exe command and wait while you run it manually (e.g. in another terminal) 3. Continue anyway, and, upon success, permanently register that this external dependency is present, but not detectable 4. Abort the installation [1/2/3/4] 1 + C:\msys64\usr\bin\pacman.exe "-Su" "--noconfirm" "mingw-w64-x86_64-libao" "mingw-w64-x86_64-pkgconf" - :: Starting core system upgrade... - there is nothing to do - :: Starting full system upgrade... - resolving dependencies... - looking for conflicting packages... - - Packages (2) mingw-w64-x86_64-libao-1.2.2-2 mingw-w64-x86_64-pkgconf-1~2.3.0-1 - - Total Download Size: 0.16 MiB - Total Installed Size: 0.77 MiB - - :: Proceed with installation? [Y/n] - :: Retrieving packages... - mingw-w64-x86_64-pkgconf-1~2.3.0-1-any downloading... - mingw-w64-x86_64-libao-1.2.2-2-any downloading... - checking keyring... - checking package integrity... - loading package files... - checking for file conflicts... - checking available disk space... - :: Processing package changes... - installing mingw-w64-x86_64-libao... - installing mingw-w64-x86_64-pkgconf... <><> Processing actions <><><><><><><><><><><><><><><><><><><><><><><><><><> 🐫 ∗ installed conf-mingw-w64-pkgconf-x86_64.1 ∗ installed conf-pkg-config.4 ∗ installed conf-ao.1 Done.Success! And the fact that conf-ao installed successfully meant that conf-ao’s build command which uses pkgconf to check whether the ao library is visible also ran successfully, so we’re hopefully in a good place to link against ao when building llama, which will be the next step.
Making Noise
Let’s try building and running an example from the llama synthesizer library.
Get the code:
PS C:\Users\s> git clone [email protected]:gridbugs/llama Cloning into 'llama'... ... Resolving deltas: 100% (961/961), done. PS C:\Users\s> cd .\llama\Since an old version of the packages in this repo are already published to Opam, we need to pin them with Opam so the local versions are used and not the released versions.
PS C:\Users\s\llama> opam pin . This will pin the following packages: llama, llama_core, llama_interactive, llama_midi, llama_tests. Continue? [y/n] y llama is now pinned to git+file://C:/Users/s/llama#main (version 0.1.0) llama_core is now pinned to git+file://C:/Users/s/llama#main (version 0.1.0) llama_interactive is now pinned to git+file://C:/Users/s/llama#main (version 0.1.0) llama_midi is now pinned to git+file://C:/Users/s/llama#main (version 0.1.0) Package llama_tests does not exist, create as a NEW package? [y/n] y llama_tests is now pinned to git+file://C:/Users/s/llama#main (version dev) [ERROR] Package conflict! * Missing dependency: - llama_tests → llama_midi >= dev no matching version [NOTE] Pinning command successful, but your installed packages may be out of sync.That error is because the llama_tests library is unreleased while all the other packages are released. Opam phones home while pinning packages to work out which versions to pin (it uses the latest released version of each released package and “dev” for unreleased packages from the look of things). That’s not ideal here though because the unreleased llama_tests package is looking for a version of llama_midi of its same version and failing to find it because the unreleased llama_tests is version “dev” while llama_midi is version “0.1.0”. I think we can work around this by forcing Opam to pin all the packages at version “dev”:
PS C:\Users\s\llama> opam pin . --with-version=dev This will pin the following packages: llama, llama_core, llama_interactive, llama_midi, llama_tests. Continue? [y/n] y [NOTE] Package llama is currently pinned to git+file://C:/Users/s/llama#main (version 0.1.0). llama is now pinned to git+file://C:/Users/s/llama#main (version dev) [NOTE] Package llama_core is currently pinned to git+file://C:/Users/s/llama#main (version 0.1.0). llama_core is now pinned to git+file://C:/Users/s/llama#main (version dev) [NOTE] Package llama_interactive is currently pinned to git+file://C:/Users/s/llama#main (version 0.1.0). llama_interactive is now pinned to git+file://C:/Users/s/llama#main (version dev) [NOTE] Package llama_midi is currently pinned to git+file://C:/Users/s/llama#main (version 0.1.0). llama_midi is now pinned to git+file://C:/Users/s/llama#main (version dev) [NOTE] Package llama_tests is already pinned to git+file://C:/Users/s/llama#main (version dev). llama_tests is now pinned to git+file://C:/Users/s/llama#main (version dev) The following actions will be performed: === install 44 packages ∗ ao 0.2.4 [required by llama] ∗ base v0.17.2 [required by ppx_inline_test] ∗ bigarray-compat 1.1.0 [required by ctypes] ∗ conf-libffi 2.0.0 [required by ctypes-foreign] ... Proceed with ∗ 44 installations? [y/n] n [NOTE] Pinning command successful, but your installed packages may be out of sync.Just checking that we now don’t have 2 pins for each released package (“0.1.0” and “dev”) from the two different runs of opam pin:
PS C:\Users\s\llama> opam pin list conf-ao.1 rsync file://C:/Users/s/conf-ao llama.dev (uninstalled) git git+file://C:/Users/s/llama#main llama_core.dev (uninstalled) git git+file://C:/Users/s/llama#main llama_interactive.dev (uninstalled) git git+file://C:/Users/s/llama#main llama_midi.dev (uninstalled) git git+file://C:/Users/s/llama#main llama_tests.dev (uninstalled) git git+file://C:/Users/s/llama#mainLooks good, and also we can see that our custom conf-ao is there which is expected. One potential issue is that the llama packages are fetched with git rather than rsync, which might bite us later if we modify the local files in the llama project without committing the results. We can avoid this by not installing any of these local packages with Opam, and instead just installing their dependencies and then building the project with Dune.
Install the deps:
PS C:\Users\s\llama> opam install . --deps-only [llama.dev] synchronised (no changes) [llama_core.dev] synchronised (no changes) [llama_interactive.dev] synchronised (no changes) [llama_midi.dev] synchronised (no changes) [llama_tests.dev] synchronised (no changes) The following actions will be performed: === install 39 packages ∗ ao 0.2.4 [required by llama] ∗ base v0.17.2 [required by ppx_inline_test] ∗ bigarray-compat 1.1.0 [required by ctypes] ∗ conf-libffi 2.0.0 [required by ctypes-foreign] ∗ conf-mingw-w64-libffi-x86_64 1 [required by conf-libffi] ∗ conf-mingw-w64-sdl2-x86_64 1 [required by conf-sdl2] ∗ conf-sdl2 1 [required by tsdl] ∗ csexp 1.5.2 [required by dune-configurator] ∗ ctypes 0.23.0 [required by tsdl] ∗ ctypes-foreign 0.23.0 [required by tsdl] ∗ dune 3.19.0 [required by llama_midi, llama_core, llama, etc.] ∗ dune-configurator 3.19.0 [required by ao] ∗ integers 0.7.0 [required by ctypes] ∗ jane-street-headers v0.17.0 [required by time_now] ∗ jst-config v0.17.0 [required by time_now] ∗ ocaml-compiler-libs v0.17.0 [required by ppxlib] ∗ ocaml_intrinsics_kernel v0.17.1 [required by base] ∗ ocamlbuild 0.16.1 [required by tsdl] ∗ ocamlfind 1.9.8 [required by tsdl] ∗ ppx_assert v0.17.0 [required by jst-config] ∗ ppx_base v0.17.0 [required by time_now] ∗ ppx_cold v0.17.0 [required by ppx_base] ∗ ppx_compare v0.17.0 [required by ppx_base] ∗ ppx_derivers 1.2.1 [required by ppxlib] ∗ ppx_enumerate v0.17.0 [required by ppx_base] ∗ ppx_globalize v0.17.0 [required by ppx_base] ∗ ppx_hash v0.17.0 [required by ppx_base] ∗ ppx_here v0.17.0 [required by ppx_assert] ∗ ppx_inline_test v0.17.0 [required by llama_tests] ∗ ppx_optcomp v0.17.0 [required by time_now] ∗ ppx_sexp_conv v0.17.0 [required by ppx_base] ∗ ppxlib 0.35.0 [required by ppx_inline_test] ∗ ppxlib_jane v0.17.2 [required by ppx_globalize, ppx_enumerate, ppx_hash] ∗ sexplib0 v0.17.0 [required by base, ppxlib] ∗ stdio v0.17.0 [required by ppx_optcomp] ∗ stdlib-shims 0.3.0 [required by ppxlib] ∗ time_now v0.17.0 [required by ppx_inline_test] ∗ topkg 1.0.8 [required by tsdl] ∗ tsdl 1.1.0 [required by llama_interactive] Proceed with ∗ 39 installations? [y/n] y The following system packages will first need to be installed: mingw-w64-x86_64-libffi mingw-w64-x86_64-SDL2 ...Note the additional depexts mingw-w64-x86_64-libffi and mingw-w64-x86_64-SDL2. Based on their names these appear to be packages in the Msys2 repo. It’s great that these packages don’t seem to require the manual intervention needed for libao. While my project’s dependencies install, let’s investigate how one of these packages works:
PS C:\Users\s> opam show conf-sdl2 ... depends "conf-pkg-config" {build} ("host-arch-x86_32" {os = "win32" & os-distribution != "cygwinports"} & "conf-mingw-w64-sdl2-i686" {os = "win32" & os-distribution != "cygwinports"} | "host-arch-x86_64" {os = "win32" & os-distribution != "cygwinports"} & "conf-mingw-w64-sdl2-x86_64" {os = "win32" & os-distribution != "cygwinports"}) ...Similar to conf-pkg-config, we see the dependency on an additional conf-mingw-w64-* metapackage under some conditions. Here though it’s os-distribution != "cygwinports" rather than os-distribution = "msys2" which makes me wonder if this would cause a problem on a regular Cygwin installation, where os-distribution = "cygwin". That would cause the os-distribution != "cygwinports" to be true which seems to select conf-mingw-w64-* packages, but my intuition is that those packages should only be selected on Msys2. There’s too much else going on to also investigate that now and maybe my hunch is wrong anyway.
There’s a new problem: llama’s dependencies failed to install:
#=== ERROR while compiling topkg.1.0.8 ========================================# # context 2.3.0 | win32/x86_64 | ocaml.5.3.0 | https://opam.ocaml.org#11859fd62a66b5e319a415e807797407068a7c13 # path ~\AppData\Local\opam\default\.opam-switch\build\topkg.1.0.8 # command ~\AppData\Local\opam\default\bin\ocaml.exe pkg/pkg.ml build --pkg-name topkg --dev-pkg false # exit-code 125 # env-file ~\AppData\Local\opam\log\topkg-18684-d088ec.env # output-file ~\AppData\Local\opam\log\topkg-18684-d088ec.out ### output ### # Exception: Fl_package_base.No_such_package ("findlib", ""). <><> Error report <><><><><><><><><><><><><><><><><><><><><><><><><><><><><> 🐫 ┌─ The following actions failed │ λ build topkg 1.0.8 └─The topkg package is a build tool. According to its description it’s in maintenance mode and should no longer be used. It’s got 165 reverse dependencies so the fact that it doesn’t build on Windows (or maybe just when using Msys2 or maybe just on my machine) is a little concerning however for our purposes here it’s only used to build tsdl which is only needed for the interactive components of llama. It would still be nice to get it working but my priority is getting the sound to work, so consider fixing topkg to be a stretch goal.
For now, let’s just install the dependencies needed for non-interactive synthesizer demos:
PS C:\Users\s\llama> opam install llama --deps-only <><> Synchronising pinned packages ><><><><><><><><><><><><><><><><><><><><> 🐫 [llama.dev] synchronised (no changes) The following actions will be performed: === install 2 packages ∗ llama_core dev (pinned) [required by llama] ∗ llama_midi dev (pinned) [required by llama_core] Proceed with ∗ 2 installations? [y/n] nGood news! All the necessary dependencies were successfully installed in the previous step. Those two deps are local packages in this project so we don’t need to install them with Opam. In theory we should now have everything we need to run an example:
Honestly that was easier than I thought it would be. I’m really happy that I didn’t need to manually install libao and that Msys2/Opam could take care of installing it and all the other system dependencies needed to build the synth library and to access the sound card.
Most of what went wrong during this process could conceivably be called “user error” though the fact that I have 5 years of OCaml programming behind me and still hit some pretty major footguns with Opam maybe suggests some aspects of its UX could be improved. Still some things were actually broken:
- conf-ao is hardcoded to run pkg-config despite the executable being named pkgconf in some cases, and has no depext for Msys2. The workaround was to pin conf-ao and fix its problems in a local copy of the package.
- Opam couldn’t find the rsync executable in the Msys2 environment despite being configured to use an Msys2 environment where rsync was installed. The workaround was to manually add C:\msys64\usr\bin to PATH.
- topkg failed to build. The workaround was to only use the non-interactive components of my project, as topkg is only necessary to compile the SDL bindings which are only needed for interactive synthesizer features.
The parts of Opam’s UX that I evidently still struggle with relate to working with local packages and pins. Looking back at the “user errors” in this post, a couple of times I tried to pin an opam file led to bizarre errors ( Unknown archive type: C:\Users\s\AppData\Local\Temp\opam-4184-d29c4e\conf-ao.opam) and unexpected results (recursively copying my home directory inside itself until I told it to stop). Opam pins are associated with package sources in the form of a directory, git repo, or archive file - not with opam files themselves. What’s confusing when working with packages like conf-ao is that these packages don’t have sources. They are metapackages that just exist to collect some dependencies on other Opam packages or external dependencies, but they have no source code or other files to fetch during installation. I’ve learnt that the correct thing to do is put the opam file I want to pin inside a directory and then pin that directory.
The other pinning-related mistake I made was pinning a project containing multiple packages where some but not all of the packages had been released. This led to the non-released packages being pinned at version “dev” while the released packages are pinned to the latest released version of those packages. If the unreleased package depends on a released package, chances are it won’t be able to resolve the dependency because it will try to find a version of the released package with version “dev”. I’ve learnt that for such projects one must run opam pin . --with-version=dev. Possibly that should be the default behaviour or at least Opam should warn when some but not all of the packages in a project are released. I’ve complained about this exact behaviour before but I still make this mistake all the time.
I have gotten used to Opam’s quirks and am no longer too phased when things go wrong. And on Windows in particular I had quite low expectations of the OCaml ecosystem since it’s historically been infamously hard to get working. This is why I’m pleasantly surprised it was this easy to get generated audio playing on Windows with OCaml, despite spending over a day on it at this point. The fact that Opam can be installed through WinGet now, and that it takes care of setting up all the external dependencies with Msys2 (or Cygwin) for you during initialization is very slick. It’s definitely come a long way since the last time I tried to use OCaml on Windows in earnest.
That said many of the problems I had with Opam in this post were not Windows-related. The problems with pinning are things I had encountered before on Unix so I could recognize them and employ workarounds. I’ve adopted a defensive approach to working with Opam where I expect the unexpected and often explicitly verify that it did what I want such as always running opam pin list after opam pin and using opam show --raw to check that pinning a package has taken effect if I’m changing a pinned package’s metadata. I don’t believe “user error” is an apt term to use here. The error is not with the user it’s with the usability of the tool.
Stretch Goal: Fixing topkg
I set aside 2 days to get llama working on Windows and it’s only been a day and a half, so let’s see if we can’t get topkg to build and then run the graphical interactive synthesizer demos which that would enable.
We’ll start by grabbing the source code for topkg. Opam has a handy feature for doing just this:
PS C:\Users\s> opam source topkg Successfully extracted to C:\Users\s\topkg.1.0.8 PS C:\Users\s> cd .\topkg.1.0.8\Now pin topkg to the local version so we can tweak things locally and test out how they affect building topkg as a dependency:
PS C:\Users\s\topkg.1.0.8> opam pin . This will pin the following packages: topkg-care, topkg. Continue? [y/n] y topkg-care is now pinned to file://C:/Users/s/topkg.1.0.8 (version 1.0.8) topkg is now pinned to file://C:/Users/s/topkg.1.0.8 (version 1.0.8) The following actions will be performed: === remove 6 packages ⊘ base-domains base [conflicts with ocaml] ⊘ base-effects base [conflicts with ocaml] ⊘ base-nnp base [uses base-domains] ⊘ ocaml-compiler 5.3.0 ⊘ ocaml_intrinsics_kernel v0.17.1 [conflicts with ocaml] ⊘ ppxlib_jane v0.17.2 [conflicts with ocaml] === downgrade 20 packages ↘ base v0.17.2 to v0.16.4 [uses ocaml] ↘ jane-street-headers v0.17.0 to v0.16.0 [uses ocaml] ↘ jst-config v0.17.0 to v0.16.0 [uses ocaml] ↘ ocaml 5.3.0 to 4.14.2 [required by topkg, topkg-care] ↘ ocaml-base-compiler 5.3.0 to 4.14.2 [required by ocaml] ↘ ocaml-compiler-libs v0.17.0 to v0.12.4 [uses ocaml] ↘ ppx_assert v0.17.0 to v0.16.0 [uses ocaml] ↘ ppx_base v0.17.0 to v0.16.0 [uses ocaml] ↘ ppx_cold v0.17.0 to v0.16.0 [uses ocaml] ↘ ppx_compare v0.17.0 to v0.16.0 [uses ocaml] ↘ ppx_enumerate v0.17.0 to v0.16.0 [uses ocaml] ↘ ppx_globalize v0.17.0 to v0.16.0 [uses ocaml] ↘ ppx_hash v0.17.0 to v0.16.0 [uses ocaml] ↘ ppx_here v0.17.0 to v0.16.0 [uses ocaml] ↘ ppx_inline_test v0.17.0 to v0.16.1 [uses ocaml] ↘ ppx_optcomp v0.17.0 to v0.16.0 [uses ocaml] ↘ ppx_sexp_conv v0.17.0 to v0.16.0 [uses ocaml] ↘ sexplib0 v0.17.0 to v0.16.0 [uses ocaml] ↘ stdio v0.17.0 to v0.16.0 [uses ocaml] ↘ time_now v0.17.0 to v0.16.0 [uses ocaml] ...Opam wants to remove or downgrade lots of packages including the compiler itself. That seems a bit drastic so let’s see if there’s a way to avoid doing that. After all topkg.1.0.8 was part of the package solution Opam found when building llama’s dependencies and ocaml.5.3.0 was sufficient in that solution. It’s odd that it wouldn’t also be sufficient here. The opam files in topkg’s source archive look like they should be compatible with ocaml.5.3.0 so I really have no idea why Opam wanted to downgrade everything.
Regardless, we don’t actually need to install topkg while pinning it. It’s pinned now so that should be sufficient for the local copy to be compiled as a dependency of llama. Let’s see if that’s correct:
PS C:\Users\s\llama> opam install . --deps-only [llama_core.dev] synchronised (no changes) [llama.dev] synchronised (no changes) [llama_interactive.dev] synchronised (no changes) [llama_midi.dev] synchronised (no changes) [llama_tests.dev] synchronised (no changes) The following actions will be performed: === install 2 packages ∗ topkg 1.0.8 (pinned) [required by tsdl] ∗ tsdl 1.1.0 [required by llama_interactive] Proceed with ∗ 2 installations? [y/n] y <><> Processing actions <><><><><><><><><><><><><><><><><><><><><><><><><><> 🐫 ⬇ retrieved tsdl.1.1.0 (cached) [ERROR] The compilation of topkg.1.0.8 failed at "ocaml pkg/pkg.ml build --pkg-name topkg --dev-pkg true". #=== ERROR while compiling topkg.1.0.8 ========================================# # context 2.3.0 | win32/x86_64 | ocaml.5.3.0 | pinned(file://C:/Users/s/topkg.1.0.8) # path ~\AppData\Local\opam\default\.opam-switch\build\topkg.1.0.8 # command ~\AppData\Local\opam\default\bin\ocaml.exe pkg/pkg.ml build --pkg-name topkg --dev-pkg true # exit-code 125 # env-file ~\AppData\Local\opam\log\topkg-6140-4d0e5a.env # output-file ~\AppData\Local\opam\log\topkg-6140-4d0e5a.out ### output ### # Exception: Fl_package_base.No_such_package ("findlib", ""). <><> Error report <><><><><><><><><><><><><><><><><><><><><><><><><><><><><> 🐫 ┌─ The following actions failed │ λ build topkg 1.0.8 └─Looks good, especially this line:
# context 2.3.0 | win32/x86_64 | ocaml.5.3.0 | pinned(file://C:/Users/s/topkg.1.0.8)To debug this problem it will help to have a command we can run to quickly reproduce it. Running the topkg’s build command, copied from its opam file, causes the same error as we see when installing topkg as a dependency:
PS C:\Users\s\topkg.1.0.8> opam exec ocaml -- .\pkg\pkg.ml build --pkg-name topkg --dev-pkg true Exception: Fl_package_base.No_such_package ("findlib", "").Before digging into the source of topkg too deeply it might pay to verify that findlib is indeed installed:
PS C:\Users\s\topkg.1.0.8> cat C:\Users\s\AppData\Local\opam\default\lib\findlib\META # specifications for "findlib": description = "Package manager" requires = "findlib.internal" requires(toploop) += "findlib.top" requires(create_toploop) += "findlib.top" version = "1.9.8" package "internal" ( version = "1.9.8" description = "Package manager" requires = "" archive(byte) = "findlib.cma" archive(native) = "findlib.cmxa" plugin(byte) = "findlib.cma" plugin(native) = "findlib.cmxs" ) package "dynload" ( version = "1.9.8" description = "Package manager dynamic loader" requires = "findlib dynlink" archive(byte) = "findlib_dynload.cma" archive(native) = "findlib_dynload.cmxa" #Even if it strange and discouraged to dynload this package plugin(byte) = "findlib_dynload.cma" plugin(native) = "findlib_dynload.cmxs" linkopts = "-linkall" ) package "top" ( version = "1.9.8" description = "Package manager toplevel support" requires = "findlib.internal" archive(byte) = "findlib_top.cma" archive(native) = "findlib_top.cmxa" )Ok this suggests that the problem is with topkg itself and not with my environment. I tried installing llama’s dependencies with Opam on Linux and topkg built fine, suggesting that the problem is specific to Windows. A likely hypothesis is that it’s related to differences with how file paths work between the Unix world and Windows, since this is a common point of pain for running OCaml programs on Windows in my experience. The main two differences that tend to cause problems are that the path separator is \ on Windows and / on Unix, and that Windows paths begin with C: while : is conventionally used to delimit multiple paths when they appear in strings on Unix systems (Windows tends to use ; for this instead to avoid confusion around the colon in C:).
Next step was to instrument topkg with debug printouts to better understand where the error was happening, but pretty quickly I realized that the problem was happening before any code in topkg could run.
Here’s a simplified version of pkg/pkg.ml from the topkg package that still exhibits the problem:
#!/usr/bin/env ocaml #use "topfind" let () = print_endline "hi"Now I can run:
PS C:\Users\s\topkg.1.0.8> opam exec ocaml -- .\pkg\pkg.ml Exception: Fl_package_base.No_such_package ("findlib", "").And of course removing the #use "topfind" from the top of that file makes the problem go away.
Interestingly I can add code before the #use "topfind" line and it still gets executed:
#!/usr/bin/env ocaml let () = print_endline "before";; #use "topfind" let () = print_endline "after";;The output is:
PS C:\Users\s\topkg.1.0.8> opam exec ocaml -- .\pkg\pkg.ml before Exception: Fl_package_base.No_such_package ("findlib", "").Searching online I found this issue which mentions the OCAML_TOPLEVEL_PATH variable. On my Windows machine this variable is set to C:\Users\s\AppData\Local\opam\default\lib\toplevel. That path refers to a folder and inside that folder is a file named topfind with some OCaml code in it. I’m not too familiar with the #use directive in OCaml but it seems to behave similarly to #include in C; acting as though the contents of the specified file replaced the occurrence of #use <file>.
Some of the code in topfind makes calls to the findlib library (part of the ocamlfind package) which is generally concerned with mapping library names to files containing compiled OCaml modules. The ocamlfind package installs a file named findlib.conf containing some metadata about library locations within an Opam switch. On my Windows machine this file was at C:\Users\s\AppData\Local\opam\default\lib\findlib.conf and its contents was:
destdir="C:\\Users\\s\\AppData\\Local\\opam\\default\\lib" path="C:\\Users\\s\\AppData\\Local\\opam\\default/lib/ocaml:C:\\Users\\s\\AppData\\Local\\opam\\default\\lib" ocamlc="ocamlc.opt" ocamlopt="ocamlopt.opt" ocamldep="ocamldep.opt" ocamldoc="ocamldoc.opt"The path field looks suspicious, since the paths on Windows begin with C:, but it looks like a colon character is also being used to separate the two paths. Recall that on Windows it’s common to use a semicolon to separate paths, rather than a colon as is typically seen on Unix. As an experiment I manually replaced the colon with a semicolon in findlib.conf and suddenly I could successfully build topkg both on its own and as a dependency for llama:
PS C:\Users\s\llama> opam install . --deps-only [llama_core.dev] synchronised (no changes) [llama.dev] synchronised (no changes) [llama_interactive.dev] synchronised (no changes) [llama_midi.dev] synchronised (no changes) [llama_tests.dev] synchronised (no changes) The following actions will be performed: === install 2 packages ∗ topkg 1.0.8 (pinned) [required by tsdl] ∗ tsdl 1.1.0 [required by llama_interactive] Proceed with ∗ 2 installations? [y/n] y <><> Processing actions <><><><><><><><><><><><><><><><><><><><><><><><><><> 🐫 ⬇ retrieved tsdl.1.1.0 (cached) ∗ installed topkg.1.0.8 ∗ installed tsdl.1.1.0 Done.So the problem wasn’t with topkg after all but rather with ocamlfind. The ocamlfind package is a bit old-school, building with a configure script and Makefile. I’m guessing somewhere in there it fails to detect that it’s running on Windows, at least in Msys2 environments, and uses the wrong path delimiter for paths in findlib.conf.
With that problem adequately worked around we should finally be able to run a graphical example:
PS C:\Users\s\llama> dune exec .\examples\interactive.exe File "examples/dune", line 44, characters 14-25: 44 | (public_name interactive) ^^^^^^^^^^^ C:/msys64/mingw64/bin/../lib/gcc/x86_64-w64-mingw32/14.2.0/../../../../x86_64-w64-mingw32/bin/ld.exe: C:/msys64/mingw64/bin/../lib/libmingw32.a(lib64_libmingw32_a-ucrtexewin.o): in function `wmain': C:/M/B/src/mingw-w64/mingw-w64-crt/crt/crtexewin.c:67:(.text+0xb5): undefined reference to `wWinMain' collect2.exe: error: ld returned 1 exit status ** Fatal error: Error during linking File "caml_startup", line 1: Error: Error during linking (exit code 2)Not quite yet!
Fixing the linker error
Linker errors like this usually mean that we’re not linking against a certain shared library, in this case whichever shared library would define the symbol wWinMain. I assumed the tsdl library would have taken care of making sure the appropriate linker flags were passed. It works fine on Linux and MacOS but maybe there are some Windows-specific flags it’s leaving out.
pkgconf should be able to tell us what flags are necessary to use a certain library. I assume whatever flags are necessary for SDL2 should suffice here:
PS C:\Users\s\llama> pkgconf.exe sdl2 --libs -LC:/msys64/mingw64/bin/../lib -lmingw32 -mwindows -lSDL2main -lSDL2I’ll start by adding these to the library_flags field of the executable stanza of the example I’m trying to run:
(executable (public_name interactive) (modules interactive) (package llama_interactive) (libraries llama_interactive keyboard_helper) (link_flags -LC:/msys64/mingw64/bin/../lib -lmingw32 -mwindows -lSDL2main -lSDL2))Building the example:
PS C:\Users\s\llama> dune build .\examples\interactive.exe File "examples/dune", line 44, characters 14-25: 44 | (public_name interactive) ^^^^^^^^^^^ C:\Users\s\AppData\Local\opam\default\bin\ocamlopt.opt.exe: unknown option '-LC:/msys64/mingw64/bin/../lib'. Usage: ocamlopt <options> <files> Try 'ocamlopt --help' for more information.Turns out you can’t pass raw linker flags to the OCaml compiler like that. In the past I’ve had success wrapping the linker flags in -cclib "<flags>" so I tried that:
(link_flags -cclib "-LC:/msys64/mingw64/bin/../lib -lmingw32 -mwindows -lSDL2main -lSDL2")And the error message changed which is often a sign of progress:
PS C:\Users\s\llama> dune build .\examples\interactive.exe File "examples/dune", line 44, characters 14-25: 44 | (public_name interactive) ^^^^^^^^^^^ flexlink: unknown option '-mwindows'. FlexDLL version 0.44 Usage: flexlink -o <result.dll/exe> file1.obj file2.obj ... -- <extra linker arguments> -o Choose the name of the output file -exe Link the main program as an exe file -maindll Link the main program as a dll file -noflexdllobj Do not add the Flexdll runtime object (for exe) -noentry Do not use the Flexdll entry point (for dll) -noexport Do not export any symbol -norelrelocs Ensure that no relative relocation is generated -base Specify base address (Win64 only) -pthread Pass -pthread to the linker -I <dir> Add a directory where to search for files -L <dir> Add a directory where to search for files -l <lib> Library file -chain {msvc|msvc64|cygwin64|mingw|mingw64|gnat|gnat64|ld} Choose which linker to use -use-linker <cmd> Choose an alternative linker to use -use-mt <cmd> Choose an alternative manifest tool to use -x64 (Deprecated) -defaultlib <obj> External object (no export, no import) -save-temps Do not delete intermediate files -implib Do not delete the generated import library -outdef Produce a def file with exported symbols -v Increment verbosity (can be repeated) -show-exports Show exported symbols -show-imports Show imported symbols -dry Show the linker command line, do not actually run it -dump Only dump the content of object files -patch Only patch the target image (to be used with -stack) -nocygpath Do not use cygpath (default for msvc, mingw) -cygpath Use cygpath (default for cygwin) -no-merge-manifest Do not merge the manifest (takes precedence over -merge-manifest) -merge-manifest Merge manifest to the dll or exe (if generated) -real-manifest Use the generated manifest (default behavior) -default-manifest Use the default manifest (default.manifest/default_amd64.manifest) -export <sym> Explicitly export a symbol -noreexport Do not reexport symbols imported from import libraries -where Show the FlexDLL directory -nounderscore Normal symbols are not prefixed with an underscore -nodefaultlibs Do not assume any default library -builtin Use built-in linker to produce a dll -explain Explain why library objects are linked -subsystem <id> Set the subsystem (default: console) -custom-crt Use a custom CRT -stack <int> Set the stack reserve in the resulting image -link <option> Next argument is passed verbatim to the linker -g (Ignored) -D <symbol> (Ignored) -U <symbol> (Ignored) -- Following arguments are passed verbatim to the linker -version Print linker version and FlexDLL directory and exit -vnum Print linker version number and exit -help Display this list of options --help Display this list of options Notes: * The -I, -l and -L options do not need to be separated from their argument. * An option like /linkXXX is an abbrevation for '-link XXX'. * An option like -Wl,-XXX is an abbreviation for '-link -XXX'. * FlexDLL's object files are searched by default in the same directory as flexlink, or in the directory given by the environment variable FLEXDIR if it is defined. * Extra argument can be passed in the environment variable FLEXLINKFLAGS. Homepage: https://github.com/ocaml/flexdll File "caml_startup", line 1: Error: Error during linking (exit code 2)Getting rid of the -mwindows argument just gets us back to the original linker error, so I assume it’s important.
Reading more about wMinMain and it looks like I was wrong assuming that we need to pass additional linker flags to tell the linker which shared library to look in. Rather, the linker was looking for wWinMain as the entry point to the program called by the C runtime’s initialization code. I have to define it myself. Perhaps OCaml assumes that the entry point will be named WinMain which seems to be the entry point for Windows C programs that don’t accept unicode arguments whereas wWinMain is used when programs do accept unicode arguments (I think the “w” stands for “wide” as in “wide character” since unicode characters can be wider than a byte). For some reason depending on SDL seems to require that the entry point be wWinMain. I don’t understand why.
Following these instructions I defined a custom entry point in some C code. For starters it’s stolen from here:
#include <wchar.h> #include <stdio.h> int wWinMain (int argc, wchar_t **argv) { wprintf(L"Hello\n"); return 0; }And in the dune file:
(executable (public_name interactive) (modules interactive) (package llama_interactive) (libraries llama_interactive keyboard_helper) (foreign_stubs (language c) (names interactive_win)))Testing it out:
PS C:\Users\s\llama> dune exec .\examples\interactive.exe Done: 49% (96/194, 98 left) (jobs: 0)HelloNice, the linker error is gone. Obviously this just prints “Hello” and exits without actually calling into any OCaml code. Now I just need to change the C code to call into the original OCaml code:
#include <wchar.h> #include <stdio.h> #define CAML_INTERNALS #include "caml/misc.h" #include "caml/mlvalues.h" #include "caml/sys.h" #include "caml/callback.h" int wWinMain (int argc, wchar_t **argv) { caml_main(argv); caml_do_exit(0); return 0; }But then when I ran the example nothing happened. Adding print statements to the OCaml code didn’t have any effect, and it looks like the program is exiting somewhere inside caml_main with no error message.
Powershell provides exit codes of programs in the $LastExitCode variable:
PS C:\Users\s\llama> dune exec .\examples\interactive.exe Done: 100% (194/194, 0 left) (jobs: 0) PS C:\Users\s\llama> $LastExitCode -1073741819Converting to a 32-bit unsigned int assuming 2’s complement this is 0xC0000005 which corresponds to an “Access Violation” which is similar to a segmentation fault in the Unix world, so probably I’m doing something wrong with pointers.
For inspiration I took a look at the real OCaml main function defined here. There’s some Windows-specific parts that I was missing which I added. I also realized I was using the wrong type for my wWinMain function so I changed its arguments based on some Microsoft documentation I found. This meant I needed to convert the Windows way of representing command-line arguments into the Unix-style argv which OCaml presumably expects. I was running out of time so instead I just generated a fake value for argv. Here’s the C program I ended up with:
#include <wchar.h> #include <stdio.h> #define CAML_INTERNALS #include "caml/misc.h" #include "caml/mlvalues.h" #include "caml/sys.h" #include "caml/osdeps.h" #include "caml/callback.h" #include <windows.h> int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow) { // fake argv for now since I don't know how to convert windows arguments to argv char_os argv0[] = { (unsigned short int)'x', 0 }; char_os** argv = malloc(sizeof(char_os*) * 2); argv[0] = argv0; argv[1] = NULL; // ocaml probably uses a null terminator for its argv since there's no argc? caml_main(argv); caml_do_exit(0); return 0; }And finally it works!
Well it mostly works. There’s about a second of input latency which isn’t there when I run it on Linux or MacOS, but at least it can now open a window and get input from the keyboard and mouse.
That brings us to the end of the 2-day period I’ve allocated for working on this project. Clearly llama is not ready for use in “production” on Windows but at least I now know that it’s technically possible to make it work. All the problems I worked around the past 2 days are not problems with llama itself but various other packages in the OCaml ecosystem (at least conf-ao calling the wrong pkg-config executable and missing Msys2 depexts and ocamlfind using the wrong path delimiters on Windows). I’ll gradually work on upstreaming fixes to these problems but in the meantime I’ll be playing my synthesizer on Unix machines only.