emscripten-core / emscripten-core/emscripten

Don't Provide `-DCMAKE_TOOLCHAIN_FILE` in emcmake Unless Configuring with CMake.

Open
#14,546 5 comments 0 reactions 0 assignees View on GitHub
good first bug help wanted
Dominant language
C++
Stars
27.6k
Forks
3.6k
Avg merge
1d 1h
Merged PRs (30d)
105

Description

# Background

CMake provides numerous [commands](https://cmake.org/cmake/help/latest/manual/cmake.1.html) in addition to merely configuring projects, which can be useful for portability when using different generators. Examples of commands include:

- `--build`
- `--install`
- `--open`
- `-E` (run a command)
- `--find-package`

Unfortunately, none of these accept `-DCMAKE_TOOLCHAIN_FILE` as a valid argument.

# Example

A sample workflow would be:

```bash
emcmake cmake ..
emcmake cmake --build . --config Release
```

The second command fails, with the following error output:

```bash
emcmake cmake --build . --config Release
configure: cmake --build . --config Release -DCMAKE_TOOLCHAIN_FILE=/emsdk/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake -DCMAKE_CROSSCOMPILING_EMULATOR=/emsdk/node/14.15.5_64bit/bin/node
Unknown argument -DCMAKE_TOOLCHAIN_FILE=/emsdk/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake
Unknown argument -DCMAKE_CROSSCOMPILING_EMULATOR=/emsdk/node/14.15.5_64bit/bin/node
Usage: cmake --build [options] [-- [native-options]]
Options:
= Project binary directory to be built.
--parallel [], -j []
= Build in parallel using the given number of jobs.
If is omitted the native build tool's
default number is used.
The CMAKE_BUILD_PARALLEL_LEVEL environment variable
specifies a default parallel level when this option
is not given.
--target ..., -t ...
= Build instead of default targets.
--config = For multi-configuration tools, choose .
--clean-first = Build target 'clean' first, then build.
(To clean only, use --target 'clean'.)
--verbose, -v = Enable verbose output - if supported - including
the build commands to be executed.
-- = Pass remaining options to the native tool.
emcmake: error: 'cmake --build . --config Release -DCMAKE_TOOLCHAIN_FILE=/emsdk/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake -DCMAKE_CROSSCOMPILING_EMULATOR=/emsdk/node/14.15.5_64bit/bin/node' failed (returned 1)
```

The lines creating the issue are here:
https://github.com/emscripten-core/emscripten/blob/660dcc00598495f5cf353771995d77a13735c0c9/emcmake.py#L35-L36

# Solution

Luckily, all of the commands besides `-P` (run a [script](https://cmake.org/cmake/help/latest/manual/cmake.1.html#run-a-script)) only accept the command as the next argument. It must be `cmake --build . --config Release`, it cannot be `cmake --config Release --build .`, which simplifies the process dramatically.

There's then 3 scenarios:

1. cmake is followed directly by a command not compatible with `-DCMAKE_TOOLCHAIN_FILE=...`
2. cmake is following by `-D...` defines, and then `-P`, making it incompatible with `-DCMAKE_TOOLCHAIN_FILE=...`
3. everything else, which works out-of-the-box

For example, `cmake --build .` works but `cmake -DX=1 --build .` does not.

# Solution

In Python pseudocode, we can therefore fix this logic as follows:

```python
import sys
# Get all arguments after the initial command, emcmake.
argv = sys.argv[1:]
# Do the normal logic to verify `cmake` is the first argument
# ...

def should_add_toolchain(args):
'''Check if we should add a toolchain file.'''

unsupported = {
'--build',
'--install',
'--open',
'-E',
'--find-package',
'--help',
}

if not args:
# called as `cmake`, does nothing, do not add a toolchain.
return False
if args[0] in unsupported:
# called where a toolchain is unsupported, do not add it.
return False
# skip all arguments leading with `-D`, to see if the command
# is a script argument.
index = 0
while index < len(args) and args[index].startswith('-D'):
index += 1
if index < len(args) and args[index].startswith('-P'):
return False

# this is a configuration case, and can be called as:
# cmake ...
# cmake ...
# cmake ... -S -B
# can safely as a toolchain to the end if not provided.
return not any(i.startswith('-DCMAKE_TOOLCHAIN_FILE=') for i in argv)

if should_add_toolchain(argv[1:]):
argv.append('-DCMAKE_TOOLCHAIN_FILE=...')

shared.check_call(argv, ...)
```

Although calling `cmake` without `emcmake` works as expected after configuration, considering that `emccmake` is meant to be a helper for `cmake`, it would be nice to have it work as expected even if called in other contexts.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.