Skip to content
Merged
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
5 changes: 1 addition & 4 deletions .github/workflows/win.yml
Original file line number Diff line number Diff line change
Expand Up @@ -263,19 +263,18 @@ jobs:
if ("${{ matrix.compiler }}" -eq "mingw") {
$buildArgs += "is_mingw=true"
$buildArgs += "is_clang=false"
$buildArgs += "ten_enable_python_binding=false" # TODO(nzh): enable python binding for mingw
$buildArgs += "ten_enable_go_binding=true"
} else {
$buildArgs += "is_mingw=false" # msvc
$buildArgs += "is_clang=true" # choose to use clang-cl.exe instead of cl.exe, the latter does not work yet
$buildArgs += "vs_version=2022"
$buildArgs += "ten_enable_python_binding=true"
$buildArgs += "ten_enable_go_binding=false"
}
$buildArgs += "log_level=1"
$buildArgs += "enable_serialized_actions=true"
$buildArgs += "ten_rust_enable_gen_cargo_config=false"
$buildArgs += "ten_enable_cargo_clean=true"
$buildArgs += "ten_enable_python_binding=true"
$buildArgs += "ten_enable_nodejs_binding=false"
$buildArgs += "ten_enable_rust_incremental_build=false"
$buildArgs += "ten_manager_enable_frontend=false"
Expand Down Expand Up @@ -757,8 +756,6 @@ jobs:
matrix:
build_type: [debug, release]
compiler: [msvc, mingw]
exclude:
- compiler: mingw
steps:
- uses: actions/checkout@v4
with:
Expand Down
29 changes: 22 additions & 7 deletions build/ten_common/python/python_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ def __init__(self):
self.target_os: str
self.config_type: str
self.log_level: int
self.is_mingw: bool = False
self.ignored_cflags: list[str] = [
"-DNDEBUG",
"-Os",
Expand Down Expand Up @@ -82,16 +83,23 @@ def get_embed_flags():
}


def transform_flags_for_windows(embed_flags):
def transform_flags_for_windows(embed_flags, is_mingw=False):
transformed = {"include_dirs": [], "lib_dirs": [], "libs": [], "cflags": []}

# Transform include directories.
for include_dir in embed_flags["include_dirs"].split():
transformed["include_dirs"].append(f'/I"{include_dir}"')
if is_mingw:
# MinGW uses GCC-style -I flags
transformed["include_dirs"].append(f'-I{include_dir}')
else:
transformed["include_dirs"].append(f'/I"{include_dir}"')

# Transform library directories.
for lib_dir in embed_flags["lib_dirs"].split():
if sys.platform == "win32":
if is_mingw:
# MinGW uses GCC-style -L flags
transformed["lib_dirs"].append(f"-L{lib_dir}")
elif sys.platform == "win32":
# output without the outer quotes to avoid escaping issues
transformed["lib_dirs"].append(f"/LIBPATH:{lib_dir}")
else:
Expand All @@ -102,9 +110,15 @@ def transform_flags_for_windows(embed_flags):
# Remove '-l' if present.
if lib.startswith("-l"):
lib = lib[2:]
# On Windows platform, we need to remove one pair of quotes because of
# the MSVC linker feature.
if sys.platform == "win32":

if is_mingw:
# MinGW uses GCC-style -l flags without .lib extension
if lib.endswith(".lib"):
lib = lib[:-4] # Remove .lib extension
transformed["libs"].append(f"{lib}")
elif sys.platform == "win32":
# On Windows platform, we need to remove one pair of quotes because of
# the MSVC linker feature.
if not lib.endswith(".lib"):
lib = f"{lib}.lib"
else:
Expand All @@ -126,7 +140,7 @@ def transform_flags_for_windows(embed_flags):
def python_config_for_win(args: ArgumentInfo) -> None:
# Retrieve embed flags using sysconfig.
embed_flags = get_embed_flags()
transformed_flags = transform_flags_for_windows(embed_flags)
transformed_flags = transform_flags_for_windows(embed_flags, args.is_mingw)

if args.config_type == "cflags":
# Print include directories and any additional CFLAGS.
Expand Down Expand Up @@ -323,6 +337,7 @@ def python_config_for_linux(args: ArgumentInfo) -> None:
parser.add_argument("--target-os", type=str, required=True)
parser.add_argument("--config-type", type=str, required=True)
parser.add_argument("--log-level", type=int, required=True)
parser.add_argument("--is-mingw", action="store_true", default=False)

arg_info = ArgumentInfo()
args = parser.parse_args(namespace=arg_info)
Expand Down
82 changes: 82 additions & 0 deletions core/src/ten_runtime/addon/addon_autoload.c
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,91 @@ typedef struct ten_addon_load_all_extensions_from_app_base_dir_ctx_t {
void *cb_data;
} ten_addon_load_all_extensions_from_app_base_dir_ctx_t;

#if defined(OS_WINDOWS) && defined(__MINGW32__)
// Helper function to add MinGW bin directories from PATH to DLL search path.
// Must be called BEFORE loading any addon DLLs that depend on MinGW runtime.
// Without this function, tests that:
// 1. Based on a go app
// 2. Needs python_addon_loader.dll
// will fail with error:
// "Failed to load module:
// .../ten_packages\addon_loader\python_addon_loader\lib\python_addon_loader.dll"
// Because python_addon_loader.dll depends on libgcc_s_seh-1.dll, libstdc++-6.dll
// and libwinpthread-1.dll, and LoadLibraryExA will not find them in system PATH,
// so we must use AddDllDirectory.
static void add_mingw_dll_directories_from_path(void) {
static bool already_added = false;
if (already_added) {
return;
}
already_added = true;

const char *path_env = getenv("PATH");
if (!path_env || strlen(path_env) == 0) {
TEN_LOGW(
"PATH environment variable is empty, cannot find MinGW directories");
return;
}

TEN_LOGI("Searching for MinGW directories in PATH to add to DLL search path");

char *path_copy = _strdup(path_env);
if (!path_copy) {
return;
}

char *token = strtok(path_copy, ";");
while (token != NULL) {
// Trim leading whitespace.
while (*token == ' ' || *token == '\t') {
token++;
}
if (strlen(token) == 0) {
token = strtok(NULL, ";");
continue;
}

// Check if this looks like a MinGW bin directory (case-insensitive).
char token_lower[MAX_PATH];
strncpy(token_lower, token, MAX_PATH - 1);
token_lower[MAX_PATH - 1] = '\0';
_strlwr(token_lower);

if (strstr(token_lower, "mingw") != NULL &&
strstr(token_lower, "bin") != NULL) {
// Verify it's a real MinGW directory by checking for gcc.exe.
char test_path[MAX_PATH];
snprintf(test_path, MAX_PATH, "%s\\gcc.exe", token);

if (GetFileAttributesA(test_path) != INVALID_FILE_ATTRIBUTES) {
// Convert to wide string for AddDllDirectory.
wchar_t wpath[MAX_PATH];
if (MultiByteToWideChar(CP_UTF8, 0, token, -1, wpath, MAX_PATH) > 0) {
DLL_DIRECTORY_COOKIE cookie = AddDllDirectory(wpath);
if (cookie != NULL) {
TEN_LOGI("Added MinGW DLL directory: %s", token);
} else {
TEN_LOGW("Failed to add MinGW DLL directory: %s (error: %lu)",
token, GetLastError());
}
}
}
}

token = strtok(NULL, ";");
}

free(path_copy);
}
#endif // OS_WINDOWS && __MINGW32__

static bool load_all_dynamic_libraries_under_path(const char *path) {
TEN_ASSERT(path, "Invalid argument.");

#if defined(OS_WINDOWS) && defined(__MINGW32__)
add_mingw_dll_directories_from_path();
#endif

bool load_at_least_one = false;

ten_dir_fd_t *dir = NULL;
Expand Down
5 changes: 5 additions & 0 deletions core/src/ten_runtime/binding/python/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ ten_package("ten_python_binding_system_package") {
}

deps = [ "native:ten_runtime_python" ]

# For MinGW, also depend on the rename target to ensure libten_runtime_python.pyd exists
if (is_win && is_mingw) {
deps += [ "native:ten_runtime_python_rename" ]
}
}

if (ten_enable_ten_manager) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,48 @@
"lib",
)

for _dir in [_lib_dir, _runtime_lib_dir]:
# Collect directories to add to DLL search path
_dll_dirs = [_lib_dir, _runtime_lib_dir]

# Add MinGW bin directories from PATH to support MinGW runtime DLLs
# This is needed because:
# 1. ten_runtime.dll and ten_utils.dll both depend on libgcc_s_seh-1.dll
# 2. libgcc_s_seh-1.dll depend on libwinpthread-1.dll
# 3. Python 3.8+ doesn't use PATH for DLL loading by default
_path_env = os.environ.get("PATH", "")
if _path_env:
for _path_dir in _path_env.split(os.pathsep):
_path_dir = _path_dir.strip()
if not _path_dir:
continue

# Check if this is a MinGW bin directory by examining:
# 1. Path contains "mingw" (case-insensitive)
# 2. Path ends with "bin"
# 3. Directory contains GCC compiler or other MinGW tools
_path_dir_lower = _path_dir.lower()
_is_mingw_candidate = (
"mingw" in _path_dir_lower
and _path_dir_lower.endswith("bin")
)

if _is_mingw_candidate and os.path.isdir(_path_dir):
# Verify it's a real MinGW directory by checking for compiler
# or common MinGW tools
_mingw_tools = [
"gcc.exe",
"g++.exe",
"mingw32-make.exe",
"ld.exe",
]
for _tool in _mingw_tools:
_tool_path = os.path.join(_path_dir, _tool)
if os.path.isfile(_tool_path):
_dll_dirs.append(_path_dir)
break

# Add all collected directories to DLL search path
for _dir in _dll_dirs:
_abs_dir = os.path.abspath(_dir)
if os.path.isdir(_abs_dir):
try:
Expand Down
106 changes: 73 additions & 33 deletions core/src/ten_runtime/binding/python/native/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,21 @@ config("ten_runtime_python_config") {

python_version = "3" # "3" by default

python_cflags_args = [
"--python-version",
python_version,
"--target-os",
target_os,
"--config-type",
"cflags",
"--log-level",
"0",
]
if (is_mingw) {
python_cflags_args += [ "--is-mingw" ]
}
python_cflags = exec_script("//build/ten_common/python/python_config.py",
[
"--python-version",
python_version,
"--target-os",
target_os,
"--config-type",
"cflags",
"--log-level",
"0",
],
python_cflags_args,
"list lines")

cflags = []
Expand All @@ -31,8 +35,19 @@ config("ten_runtime_python_config") {
# https://stackoverflow.com/questions/4787069/autoconf-set-fpic-only-when-necessary
if (!is_win) {
cflags = python_cflags + [ "-fPIC" ]
} else if (is_mingw) {
# For MinGW (GCC on Windows), python_config.py already generates GCC-style flags
cflags = python_cflags

# Undefine Py_DEBUG to use release Python library even in debug builds
# Standard Python installations on Windows only provide release libraries
# Also undefine _DEBUG which Python headers use to determine debug mode
cflags += [
"-UPy_DEBUG",
"-U_DEBUG",
]
} else {
# For clang-cl on Windows, convert /I"path" to /imsvcpath (without space or quotes)
# For clang-cl(MSVC) on Windows, convert /I"path" to /imsvcpath (without space or quotes)
# /imsvc treats the path as a system include directory, which helps clang-cl find Python headers
foreach(flag, python_cflags) {
processed_flag = string_replace(flag, "/I\"", "/imsvc")
Expand All @@ -53,17 +68,21 @@ config("ten_runtime_python_config") {
# for building extension modules (not embedding Python interpreter).
# This ensures we don't link against libpython, making the extension
# compatible with different Python 3.8+ versions.
ldflags_args = [
"--python-version",
python_version,
"--target-os",
target_os,
"--config-type",
"ldflags",
"--log-level",
"0",
]
if (is_mingw) {
ldflags_args += [ "--is-mingw" ]
}
ldflags = exec_script("//build/ten_common/python/python_config.py",
[
"--python-version",
python_version,
"--target-os",
target_os,
"--config-type",
"ldflags",
"--log-level",
"0",
],
ldflags_args,
"list lines")

if (is_mac) {
Expand Down Expand Up @@ -101,17 +120,21 @@ config("ten_runtime_python_config") {

# Note: python_config.py now uses python-config without --embed flag,
# so libs won't include libpython for extension modules.
libs_args = [
"--python-version",
python_version,
"--target-os",
target_os,
"--config-type",
"libs",
"--log-level",
"0",
]
if (is_mingw) {
libs_args += [ "--is-mingw" ]
}
libs = exec_script("//build/ten_common/python/python_config.py",
[
"--python-version",
python_version,
"--target-os",
target_os,
"--config-type",
"libs",
"--log-level",
"0",
],
libs_args,
"list lines")
}

Expand Down Expand Up @@ -143,8 +166,25 @@ ten_shared_library("ten_runtime_python") {
output_extension = "so"
} else if (is_win) {
# On Windows, Python C extensions must be .pyd files.
# We also prepend "lib" to match the module name "libten_runtime_python".
output_name = "libten_runtime_python"
# Python requires the module name: "from libten_runtime_python import ..."
# For MinGW: Use output_name="ten_runtime_python" to generate:
# - ten_runtime_python.pyd (will be renamed to libten_runtime_python.pyd)
# - libten_runtime_python.pyd.a (import library with auto "lib" prefix)
# For MSVC: Use output_name="libten_runtime_python" directly.
if (is_mingw) {
output_name = "ten_runtime_python"
} else {
output_name = "libten_runtime_python"
}
output_extension = "pyd"
}
}

# For MinGW, rename the .pyd file to include "lib" prefix for Python import
if (is_win && is_mingw) {
copy("ten_runtime_python_rename") {
sources = [ "${root_out_dir}/ten_runtime_python.pyd" ]
outputs = [ "${root_out_dir}/libten_runtime_python.pyd" ]
deps = [ ":ten_runtime_python" ]
}
}
Loading
Loading