Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion data/test.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@
"msvc",
"gcc",
"cygwin",
"!cygwin"
"!cygwin",
"windows",
"!windows"
]
},
"version": {
Expand Down
2 changes: 2 additions & 0 deletions docs/markdown/Contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,8 @@ considered if the platform matches. The following values for
| `gcc` | Not `msvc` |
| `cygwin` | Matches when the platform is cygwin |
| `!cygwin` | Not `cygwin` |
| `windows` | Matches when the platform is windows or cygwin |
| `!windows` | Not `windows` |

#### matrix

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
## The external_project module uses the cygpath command to convert paths

In previous versions, the external_project module on Windows used a Windows-style path (e.g., `C:/path/to/configure`) to execute the configure file, and a relative path from the drive root (e.g., `/path/to/prefix`) as the installation prefix.
However, since configure scripts are typically intended to be run in a POSIX-like environment (MSYS2, Cygwin, or GitBash), these paths were incompatible with some configure scripts.

The external_project module now uses the `cygpath` command to convert the configure command path and prefix to Unix-style paths (e.g., `/c/path/to/configure` for MSYS2 and `/cygdrive/c/path/to/configure` for Cygwin).
If the `cygpath` command is not found in the PATH, it will fall back to the previous behavior.
27 changes: 20 additions & 7 deletions mesonbuild/modules/external_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from pathlib import Path
import os
import shlex
import shutil
import subprocess
import typing as T

Expand All @@ -19,7 +20,7 @@
from ..interpreter.type_checking import ENV_KW, DEPENDS_KW
from ..interpreterbase.decorators import ContainerTypeInfo, KwargInfo, typed_kwargs, typed_pos_args
from ..mesonlib import (EnvironmentException, MesonException, Popen_safe, MachineChoice,
get_variable_regex, do_replacement, join_args, relpath)
get_variable_regex, do_replacement, join_args)
from ..options import OptionKey

if T.TYPE_CHECKING:
Expand Down Expand Up @@ -89,19 +90,29 @@ def __init__(self,
self.includedir = Path(_i)
self.name = self.src_dir.name

# On Windows if the prefix is "c:/foo" and DESTDIR is "c:/bar", `make`
# will install files into "c:/bar/c:/foo" which is an invalid path.
# Work around that issue by removing the drive from prefix.
if self.prefix.drive:
self.prefix = Path(relpath(self.prefix, self.prefix.drive))
self.prefix = self._cygpath_convert(self.prefix)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the case cygpath is not found we don't remove the drive from self.prefix anymore. That means we now run configure --prefix=c:/foo instead of configure --prefix=/foo. Is that intentional?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if you also need to convert self.install_dir as that's used for DESTDIR?

Copy link
Contributor Author

@na-trium-144 na-trium-144 Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That means we now run configure --prefix=c:/foo instead of configure --prefix=/foo.

I don't know which is appropriate in that case, but the latter has the problem of not working with another drive. If anyone has an environment with sh but no cygpath, they may submit an issue or PR.

I'm wondering if you also need to convert self.install_dir as that's used for DESTDIR?

Unlike the prefix used by configure, DESTDIR is used by make, right? So far I haven't had any issues with the drive letter for install_dir.


# self.prefix is an absolute path, so we cannot append it to another path.
self.rel_prefix = Path(relpath(self.prefix, self.prefix.root))
# On Windows (where cygpath is not applied),
# if the prefix is "c:/foo" and DESTDIR is "c:/bar",
# `make` will install files into "c:/bar/c:/foo" which is an invalid path.
# This also removes the drive letter from the prefix to workaround the issue.
self.rel_prefix = self.prefix.relative_to(self.prefix.anchor)

self._configure(state)

self.targets = self._create_targets(extra_depends)

def _cygpath_convert(self, winpath: Path) -> Path:
# On Cygwin, MSYS2 and GitBash, the configure command and the prefix
# should be converted to unix style path like "/c/foo" by cygpath command,
# because the colon in the drive letter breaks many configure scripts.
# Do nothing on other environment where cygpath is not available.
if winpath.drive and shutil.which('cygpath'):
_p, o, _e = Popen_safe(['cygpath', '-u', winpath.as_posix()])
return Path(o.strip('\n'))
return winpath

def _configure(self, state: 'ModuleState') -> None:
if self.configure_command == 'waf':
FeatureNew('Waf external project', '0.60.0').use(self.subproject, state.current_node)
Expand All @@ -116,6 +127,8 @@ def _configure(self, state: 'ModuleState') -> None:
configure_path = Path(self.src_dir, self.configure_command)
configure_prog = state.find_program(configure_path.as_posix())
configure_cmd = configure_prog.get_command()
if len(configure_cmd) >= 2 and configure_cmd[-1] == configure_path.as_posix():
configure_cmd = configure_cmd[:-1] + [self._cygpath_convert(configure_path).as_posix()]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this needed? run_command('configure') would not do this and I assume it's working.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the main point of this PR. see #13699.

workdir = self.build_dir
self.make = state.find_program('make').get_command()

Expand Down
2 changes: 2 additions & 0 deletions run_project_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@ def get_path(self, compiler: str, env: environment.Environment) -> T.Optional[Pa
'gcc': canonical_compiler != 'msvc',
'cygwin': env.machines.host.is_cygwin(),
'!cygwin': not env.machines.host.is_cygwin(),
'windows': env.machines.host.is_windows() or env.machines.host.is_cygwin(),
'!windows': not (env.machines.host.is_windows() or env.machines.host.is_cygwin()),
}.get(self.platform or '', True)
if not matches:
return None
Expand Down
17 changes: 17 additions & 0 deletions test cases/common/230 external project/libfoo/configure
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,23 @@

srcdir=$(dirname "$0")

# some configure scripts seem to iterate over srcdir and other paths
# with for-loop using path_separator (most cases colon.)
PATH_SEPARATOR=:
(PATH='/bin;/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 && {
(PATH='/bin:/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 ||
PATH_SEPARATOR=';'
}
IFS=$PATH_SEPARATOR
for i in $srcdir
do
if [ "$i" != "$srcdir" ]
then
echo "failed to extract $srcdir using path separator $PATH_SEPARATOR"
exit 1
fi
done
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand what this is doing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a test for the fix related to the comment above.
I wrote it to resemble the processing done by the existing configure script, but it might be the same if I simply checked whether srcdir contains a colon.


for i in "$@"
do
case $i in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ p = mod.add_project('configure',
'--libext=' + libext,
],
depends: somelib,
verbose: true,
)

libfoo_dep = declare_dependency(link_with : somelib,
Expand Down
4 changes: 0 additions & 4 deletions test cases/common/230 external project/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,6 @@ if not find_program('make', required : false).found()
error('MESON_SKIP_TEST: make not found')
endif

if host_machine.system() == 'windows'
error('MESON_SKIP_TEST: The fake configure script is too dumb to work on Windows')
endif

if meson.is_cross_build()
# CI uses PKG_CONFIG_SYSROOT_DIR which breaks -uninstalled.pc usage.
error('MESON_SKIP_TEST: Cross build support is too limited for this test')
Expand Down
4 changes: 2 additions & 2 deletions test cases/common/230 external project/test.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"installed": [
{ "type": "shared_lib", "file": "usr/lib/foo", "platform": "!cygwin" },
{ "type": "file", "file": "usr/lib/libfoo.dll", "platform": "cygwin" },
{ "type": "shared_lib", "file": "usr/lib/foo", "platform": "!windows" },
{ "type": "file", "file": "usr/lib/libfoo.dll", "platform": "windows" },
{ "type": "file", "file": "usr/include/libfoo.h" },
{ "type": "file", "file": "usr/lib/pkgconfig/somelib.pc" }
]
Expand Down
Loading