diff --git a/conan_config/profiles/emsdk/.base b/conan_config/profiles/emsdk/.base new file mode 100644 index 0000000..8bc0818 --- /dev/null +++ b/conan_config/profiles/emsdk/.base @@ -0,0 +1,23 @@ +[settings] +build_type=Release +compiler=emcc +compiler.cppstd=17 +compiler.libcxx=libc++ +# Choose between both types of multithreading support (or none) +# compiler.threads= +compiler.version=4.0.22 +os=Emscripten + +[tool_requires] +ninja/[*] +emsdk/4.0.22 + +[conf] +tools.build:exelinkflags+=['-sALLOW_MEMORY_GROWTH=1'] +tools.build:sharedlinkflags+=['-sALLOW_MEMORY_GROWTH=1'] + +# Set Ninja as default generator as it will avoid Windows issues +tools.cmake.cmaketoolchain:generator=Ninja + +# Distinguish between architectures +tools.cmake.cmake_layout:build_folder_vars=['settings.build_type', 'settings.arch'] diff --git a/conan_config/profiles/emsdk/README.md b/conan_config/profiles/emsdk/README.md new file mode 100644 index 0000000..bc4f6d7 --- /dev/null +++ b/conan_config/profiles/emsdk/README.md @@ -0,0 +1,150 @@ +# EMSDK - Emscripten Compiler Profiles for Conan + +This repository provides a collection of pre-built Conan profiles that can be +used as a reference for **cross-compiling projects to WebAssembly (WASM)**. + +📘 For detailed information and usage examples, refer to the official [Conan +documentation](https://docs.conan.io/2/examples/cross_build/emscripten.html#setting-up-conan-profile-for-webassembly-wasm). + +💡 Issues and suggestions are welcome via the `conan-toolchains` repository. +Contributions are encouraged! + + +## 🧭 Introduction + +As of **Conan 2.18**, the `emcc` compiler (from +[Emscripten](https://emscripten.org/docs/)) is natively supported. This allows +accurate modeling of the compiler’s built-in features. + +The current `emcc` compiler model includes the following settings: + +``` +emcc: + version: [ANY] + libcxx: [null, libstdc++, libstdc++11, libc++] + threads: [null, posix, wasm_workers] + cppstd: [null, 98, gnu98, 11, gnu11, 14, gnu14, 17, gnu17, 20, gnu20, 23, gnu23, 26, gnu26] + cstd: [null, 99, gnu99, 11, gnu11, 17, gnu17, 23, gnu23] +``` + +> ℹ️ Note: `emcc` is a front-end for the `clang` compiler, so it shares many of the same settings and options. + +However, there are a few **important caveats** to consider when using Emscripten with Conan. + + +## ⚠️ Caveats + +### 🔄 ABI Incompatibility + +There is **no ABI compatibility guarantee** between different `emcc` versions. +Refer to the [Emscripten Changelog](https://github.com/emscripten-core/emscripten/blob/main/ChangeLog.md) for details. + +In Conan v2, the `compiler.version` setting mostly acts as **syntactic sugar**. While it doesn't influence compilation directly, it: + +- Enables Conan to apply version-specific flags, compile and link flags. +- Ensures distinct **package IDs** for different compiler versions. + +To prevent ABI issues, make sure your **Conan profile's `compiler.version`** +matches the **actual `emsdk` version** used. This ensures consistent builds and +automatic recompilation when updating `emsdk`. + + + +### 🧵 Multithreading + +The `emcc` compiler can produce incompatible binaries depending on whether threading is enabled. + +Refer to the Emscripten documentation on [pthreads](https://emscripten.org/docs/porting/pthreads.html#compiling-with-pthreads-enabled) +and [wasm workers](https://emscripten.org/docs/api_reference/wasm_workers.html). + +Emscripten supports two threading models: + +- **POSIX Threads (Pthreads)** +- **Wasm Workers** + +These are **mutually incompatible**, and this distinction must be reflected in the Conan profile to avoid mixing binaries. + +Enable threading in your profile by setting: + +```ini +compiler.threads=posix # For Pthreads +compiler.threads=wasm_workers # For Wasm Workers +``` + +Conan will automatically inject the necessary linker flags. + +## 📂 Profile Overview + +This repository includes the following profiles: +- `wasm32`: WebAssembly 32-bit target (default). +- `wasm64`: Experimental 64-bit WebAssembly target (for projects needing >4 GB dynamic memory). + + +⚠️ WASM64 caveats: + +The latest node version `emsdk/4.0.22` installs is the `node/22.16.0`, which can not run directly wasm64 binaries. +Also, it is not valid to compile wasm64 with `-sMIN_NODE_VERSION=221600`, see following error: +``` +em++: warning: MIN_NODE_VERSION=221600 is not compatible with MEMORY64 (230000 or above required) [-Wcompatibility] +``` +Thus, to be able to run an `wasm64` binary you will need to download at least `node/23` manually in your system. + +### 🛠️ Local emsdk installation + +If you wish to use your locally installed emsdk instead of the Conan-managed one, here is the proposed emsdk profile + +``` +include(./.base) + +[platform_tool_requires] +emsdk/[*] + +[conf] +tools.build:compiler_executables={'c':'emcc', 'cpp':'em++'} +# Add local Emscripten toolchain +# tools.cmake.cmaketoolchain:user_toolchain=["/path/to/emsdk/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake"] + +[buildenv] +CC=emcc +CXX=em++ +AR=emar +NM=emnm +RANLIB=emranlib +STRIP=emstrip +``` + +The `platform_tool_requires` will tell Conan to not use the `emsdk` recipe but your system installed binary + +- Ensure `emcc`, `em++`, `emar`, etc. are available in your `PATH`. +- Check the `[buildenv]` section in the profile. +- Optionally, provide your own `Emscripten.cmake` via the `user_toolchains` setting. +- Define the arch setting: `wasm`, `wasm64`, or the mostly deprecated `asm.js`. + + +## ▶️ Usage + +After installing the profiles (TBD), you can build your project like this: + +``` +$ conan build -pr emsdk/wasm32 +``` + +### 🧠 Dynamic Memory Allocation + +By default, WebAssembly does not allow dynamic memory growth. To enable it, you must set the following linker flag: + +`-s ALLOW_MEMORY_GROWTH=1` + +Our base profiles enable this flag by default to simplify usage. + +If you want to disable memory growth, simply remove or comment out the relevant line in the profile. + +🔎 The Conan docs explain the dynamic memory limits for each architecture. +These are also preconfigured in the `wasm32` and `wasm64` profiles and can be +customized as needed, including the `INITIAL_MEMORY` setting. + + +### 🙌 Contribute + +Feel free to open issues or pull requests in the `conan-toolchains` repository. +Contributions to extend or improve these profiles are welcome! diff --git a/conan_config/profiles/emsdk/wasm32 b/conan_config/profiles/emsdk/wasm32 new file mode 100644 index 0000000..be675ee --- /dev/null +++ b/conan_config/profiles/emsdk/wasm32 @@ -0,0 +1,8 @@ +include(./.base) + +[settings] +arch=wasm + +[conf] +tools.build:exelinkflags+=['-sMAXIMUM_MEMORY=4GB', '-sINITIAL_MEMORY=64MB'] +tools.build:sharedlinkflags+=['-sMAXIMUM_MEMORY=4GB', '-sINITIAL_MEMORY=64MB'] diff --git a/conan_config/profiles/emsdk/wasm64 b/conan_config/profiles/emsdk/wasm64 new file mode 100644 index 0000000..be98ce4 --- /dev/null +++ b/conan_config/profiles/emsdk/wasm64 @@ -0,0 +1,12 @@ +include(./.base) + +[settings] +arch=wasm64 + +[conf] +# In this early stage of wasm64, ALLOW_MEMORY_GROWTH is not having effect. Also it may not be the most efficient solution. +# wasm64 for now needs to declare INITIAL_MEMORY as the maximum memory +tools.build:exelinkflags+=['-sMAXIMUM_MEMORY=16GB', '-sINITIAL_MEMORY=16GB', '-sASSERTIONS'] +tools.build:sharedlinkflags+=['-sMAXIMUM_MEMORY=16GB', '-sINITIAL_MEMORY=16GB', '-sASSERTIONS'] + +# Node version from emsdk/4.0.22 can not run a wasm64 binary. See more details in README diff --git a/recipes/emsdk/all/conandata.yml b/recipes/emsdk/all/conandata.yml new file mode 100644 index 0000000..b41d0ca --- /dev/null +++ b/recipes/emsdk/all/conandata.yml @@ -0,0 +1,4 @@ +sources: + "4.0.22": + url: "https://github.com/emscripten-core/emsdk/archive/4.0.22.tar.gz" + sha256: "8f5e26de5c103c41ae04411e1be1ffc17983fb368343b15102a1859b3aa64626" diff --git a/recipes/emsdk/all/conanfile.py b/recipes/emsdk/all/conanfile.py new file mode 100644 index 0000000..0fea66a --- /dev/null +++ b/recipes/emsdk/all/conanfile.py @@ -0,0 +1,107 @@ +import os +from pathlib import Path + +from conan import ConanFile +from conan.tools.env import VirtualBuildEnv +from conan.tools.files import chdir, copy, get +from conan.tools.layout import basic_layout + +required_conan_version = ">=2.1" + + +class EmSDKConan(ConanFile): + name = "emsdk" + description = "Emscripten SDK. Emscripten is an Open Source LLVM to JavaScript compiler" + url = "https://github.com/conan-io/conan-center-index" + homepage = "https://github.com/kripken/emscripten" + topics = ("emsdk", "emscripten", "sdk", "emcc", "em++", "nodejs") + license = "MIT" + package_type = "application" + settings = "os", "arch" + + def layout(self): + basic_layout(self, src_folder="src") + + def source(self): + get(self, **self.conan_data["sources"][self.version], destination=self.source_folder, strip_root=True) + + @property + def _emscripten(self): + return os.path.join(self.package_folder, "bin", "upstream", "emscripten") + + @property + def _node_path(self): + subfolders = [path for path in (Path(self.package_folder) / "bin" / "node").iterdir() if path.is_dir()] + if len(subfolders) != 1: + return None + return os.path.join("bin", "node", subfolders[0].name, "bin") + + def generate(self): + # To avoid issues when cross-compiling or with not common arch in profiles we need to set EMSDK_ARCH + # This is important for the emsdk install command + env = VirtualBuildEnv(self) + # Special consideration for armv8 as emsdk expects "arm64" + arch = "arm64" if str(self.settings.arch) == "armv8" else str(self.settings.arch) + env.environment().define("EMSDK_ARCH", arch) + env.generate() + + def build(self): + with chdir(self, self.source_folder): + emsdk = "emsdk.bat" if self.settings_build.os == "Windows" else "./emsdk" + self.run(f"{emsdk} install latest") + self.run(f"{emsdk} activate latest") + + def package(self): + copy(self, "LICENSE", src=self.source_folder, dst=os.path.join(self.package_folder, "licenses")) + copy(self, "*", src=self.source_folder, dst=os.path.join(self.package_folder, "bin")) + + def finalize(self): + copy(self, "*", src=self.immutable_package_folder, dst=self.package_folder) + embuilder = os.path.join( + self._emscripten, "embuilder" if self.info.settings.os != "Windows" else "embuilder.bat" + ) + self.run(f"{embuilder} build MINIMAL") + + def _define_tool_var(self, value): + suffix = ".bat" if self.settings.os == "Windows" else "" + path = os.path.join(self._emscripten, f"{value}{suffix}") + return path + + def package_info(self): + self.cpp_info.bindirs = ["bin", os.path.join("bin", "upstream", "emscripten"), self._node_path] + self.cpp_info.includedirs = [] + self.cpp_info.libdirs = [] + self.cpp_info.resdirs = [] + + # If we are not building for Emscripten, probably we don't want to inject following environment variables, + # but it might be legit use cases... until we find them, let's be conservative. + if not hasattr(self, "settings_target") or self.settings_target is None: + return + + if self.settings_target.os != "Emscripten": + self.output.warning( + f"You've added {self.name}/{self.version} as a build requirement, while os={self.settings_target.os} != Emscripten" + ) + return + + toolchain = os.path.join( + self.package_folder, "bin", "upstream", "emscripten", "cmake", "Modules", "Platform", "Emscripten.cmake" + ) + self.conf_info.prepend("tools.cmake.cmaketoolchain:user_toolchain", toolchain) + + self.buildenv_info.define_path("EMSDK", os.path.join(self.package_folder, "bin")) + self.buildenv_info.define_path("EMSCRIPTEN", self._emscripten) + self.buildenv_info.define_path("EM_CONFIG", os.path.join(self.package_folder, "bin", ".emscripten")) + self.buildenv_info.define_path("EM_CACHE", os.path.join(self.package_folder, "bin", ".emscripten_cache")) + + compiler_executables = { + "c": self._define_tool_var("emcc"), + "cpp": self._define_tool_var("em++"), + } + self.conf_info.update("tools.build:compiler_executables", compiler_executables) + self.buildenv_info.define_path("CC", compiler_executables["c"]) + self.buildenv_info.define_path("CXX", compiler_executables["cpp"]) + self.buildenv_info.define_path("AR", self._define_tool_var("emar")) + self.buildenv_info.define_path("NM", self._define_tool_var("emnm")) + self.buildenv_info.define_path("RANLIB", self._define_tool_var("emranlib")) + self.buildenv_info.define_path("STRIP", self._define_tool_var("emstrip")) diff --git a/recipes/emsdk/all/test_package/conanfile.py b/recipes/emsdk/all/test_package/conanfile.py new file mode 100644 index 0000000..a0948be --- /dev/null +++ b/recipes/emsdk/all/test_package/conanfile.py @@ -0,0 +1,16 @@ +from conan import ConanFile +from conan.tools.build import can_run + + +class TestPackageConan(ConanFile): + settings = "os", "arch", "compiler", "build_type" + + def requirements(self): + self.requires(self.tested_reference_str) + + def test(self): + # Check the package provides working binaries + if can_run(self): + self.run("emcc -v", env="conanrun") + self.run("em++ -v", env="conanrun") + self.run("node -v", env="conanrun") diff --git a/recipes/emsdk/config.yml b/recipes/emsdk/config.yml new file mode 100644 index 0000000..6764e36 --- /dev/null +++ b/recipes/emsdk/config.yml @@ -0,0 +1,3 @@ +versions: + "4.0.22": + folder: all