Repository: lhnguyen102/cuTAGI
Pull request #118: Hotfix/windows cpu compilation
Author: login: miquelflorensa
Labels: name: bug
description: Something isn't working
Created: 2025-04-03T19:27:00Z | Merged: 2025-04-06T21:56:07Z

## Description

This pull request fixes Windows build issues for the `pytagi` package by improving cross-platform compatibility and adding proper CI support. It resolves test build failures during `pip install` on Windows and ensures wheels can be built reliably across Python versions.

---

## Changes Made

- **Thread Handling Fix**: Replaced C-style fixed-length thread arrays with `std::vector<std::thread>` to ensure compatibility with MSVC.
- **CMake Option**: Added `option(BUILD_TESTING "Build and run tests" ON)` to conditionally include test builds. This prevents CTest from being invoked during wheel packaging on Windows.
- **Windows Wheel CI**:
  - Added a `build_windows` job in `build_wheels.yml` for Python 3.10–3.12.
  - Uses `cibuildwheel` to build CPU-only wheels.
  - Disables test builds via `CMAKE_ARGS='-DBUILD_TESTING=OFF'`.
  - Downgrades NumPy to `<2.0` to avoid ABI issues on Windows with Anaconda.

---

## Related Issue(s)

Issue #94 

This PR allows to build on Windows machine for CPU executions.

---

## Checklist

- [x] I have followed the project's coding conventions and style guidelines.
- [x] I have updated the documentation, if applicable.
- [x] I have rebased my branch on the latest upstream code to incorporate any changes.
- [x] I have tested the changes locally.


Diff:
diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml
index 579eb39d5..60438a6ac 100644
--- a/.github/workflows/build_wheels.yml
+++ b/.github/workflows/build_wheels.yml
@@ -106,6 +106,48 @@ jobs:
           name: macos-wheels-${{ matrix.python_version }}
           path: ./wheelhouse/*.whl
 
+  build_windows:
+    name: Windows Build (CPU Only)
+    runs-on: windows-latest
+    strategy:
+      matrix:
+        include:
+          - python_version: "3.10"
+            cibw_build: "cp310-win_amd64"
+          - python_version: "3.11"
+            cibw_build: "cp311-win_amd64"
+          - python_version: "3.12"
+            cibw_build: "cp312-win_amd64"
+    if: ${{ github.event.pull_request.draft == false }}
+    steps:
+      - uses: actions/checkout@v4
+        with:
+          submodules: recursive
+
+      - name: Set up Python ${{ matrix.python_version }}
+        uses: actions/setup-python@v5
+        with:
+          python-version: ${{ matrix.python_version }}
+
+      - name: Install cibuildwheel and NumPy <2.0
+        run: |
+          python -m pip install cibuildwheel==2.21.3
+          python -m pip install "numpy<2.0"
+
+      - name: Build wheels (CPU Only)
+        run: |
+          python -m cibuildwheel --output-dir wheelhouse
+        env:
+          CIBW_BUILD: ${{ matrix.cibw_build }}
+          CIBW_ARCHS_WINDOWS: auto64
+          CIBW_BUILD_VERBOSITY: 1
+          CIBW_ENVIRONMENT: "FORCE_CPU_ONLY=1 CMAKE_ARGS='-DBUILD_TESTING=OFF'"
+
+      - uses: actions/upload-artifact@v4
+        with:
+          name: windows-wheels-cpu-${{ matrix.python_version }}
+          path: ./wheelhouse/*.whl
+
   build_sdist:
     name: SDist Build
     runs-on: ${{ matrix.os }}
@@ -140,7 +182,7 @@ jobs:
 
   test:
     name: Test
-    needs: [build_linux, build_macos, build_sdist]
+    needs: [build_linux, build_macos, build_windows, build_sdist]
     runs-on: ubuntu-22.04
     if: ${{ github.event.pull_request.draft == false }}
     steps:
@@ -193,11 +235,11 @@ jobs:
         with:
           path: ./artifacts
 
-      - name: Collect all build files
+      - name: Collect and clean all build files
         run: |
           mkdir dist
           find ./artifacts -type f \( -name "*.whl" -o -name "*.tar.gz" \) -exec cp {} dist/ \;
-
+          rm -rf build *.egg-info  # Clean any stale metadata
 
       - name: Install twine
         run: |
@@ -210,4 +252,4 @@ jobs:
           TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
         run: |
           twine upload --skip-existing dist/*
-# twine upload --repository-url https://test.pypi.org/legacy/ --skip-existing dist/*
\ No newline at end of file
+# twine upload --repository-url https://test.pypi.org/legacy/ --skip-existing dist/*
diff --git a/CMakeLists.txt b/CMakeLists.txt
index fb68b4af0..66f61e824 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -102,6 +102,7 @@ endif()
 # find_package(Python COMPONENTS Interpreter Development)
 # find_package(PythonInterp)
 find_package(Python REQUIRED COMPONENTS Interpreter Development.Module)
+option(BUILD_TESTING "Build and run tests" ON)
 
 ################################################################################
 # ADD SUBMODULES AS SUBDIRECTORIES
@@ -183,7 +184,6 @@ else()
   # Executable code i.e. application
   add_executable(main main.cpp)
 
-
   # TODO: Remove when releasing new version
   pybind11_add_module(cutagi "src/bindings/main_bindings.cpp")
 endif()
@@ -198,33 +198,33 @@ target_link_libraries(cutagi PRIVATE cutagi_lib)
 ################################################################################
 # TESTS
 ################################################################################
-enable_testing()
-
-# Glob the test sources
-file(GLOB TEST_SOURCES "test/cpp_unit/*.cpp")
-
-# Add main.cpp separately to specify the entry point
-set(TEST_MAIN "test/cpp_unit/main.cpp")
-
-# Create an executable for the tests
-add_executable(run_tests ${TEST_MAIN} ${TEST_SOURCES})
-
-# Set include directories for the test executable
-target_include_directories(run_tests PRIVATE
-  ${pybind11_INCLUDE_DIRS}
-  ${CMAKE_CURRENT_SOURCE_DIR}/test/cpp_unit
-)
-
-# Link the test executable with the required libraries and cutagi_lib
-target_link_libraries(run_tests PRIVATE
-  ${Python_INCLUDE_DIRS}
-  pybind11::embed
-  gtest_main
-  cutagi_lib
-)
-
-# Discover GoogleTest tests
-include(GoogleTest)
-gtest_discover_tests(run_tests)
-
-
+if(BUILD_TESTING)
+  enable_testing()
+
+  # Glob the test sources
+  file(GLOB TEST_SOURCES "test/cpp_unit/*.cpp")
+
+  # Add main.cpp separately to specify the entry point
+  set(TEST_MAIN "test/cpp_unit/main.cpp")
+
+  # Create an executable for the tests
+  add_executable(run_tests ${TEST_MAIN} ${TEST_SOURCES})
+
+  # Set include directories for the test executable
+  target_include_directories(run_tests PRIVATE
+    ${pybind11_INCLUDE_DIRS}
+    ${CMAKE_CURRENT_SOURCE_DIR}/test/cpp_unit
+  )
+
+  # Link the test executable with the required libraries and cutagi_lib
+  target_link_libraries(run_tests PRIVATE
+    ${Python_INCLUDE_DIRS}
+    pybind11::embed
+    gtest_main
+    cutagi_lib
+  )
+
+  # Discover GoogleTest tests
+  include(GoogleTest)
+  gtest_discover_tests(run_tests)
+endif()
\ No newline at end of file
diff --git a/src/derivative_calcul_cpu.cpp b/src/derivative_calcul_cpu.cpp
index c0dbd998d..7437a9a2f 100644
--- a/src/derivative_calcul_cpu.cpp
+++ b/src/derivative_calcul_cpu.cpp
@@ -438,7 +438,7 @@ void compute_node_derv_mean_var_fc_mt(
     const int n_batch = tot_ops / num_threads;
     const int rem_batch = tot_ops % num_threads;
     int start_idx, end_idx;
-    std::thread threads[num_threads];
+    std::vector<std::thread> threads(NUM_THREADS);
 
     for (int i = 0; i < num_threads; i++) {
         if (i == 0) {
@@ -544,7 +544,7 @@ void compute_cov_d_dw_fc_mt(std::vector<float> &mda, std::vector<float> &ma,
     const int n_batch = tot_ops / num_threads;
     const int rem_batch = tot_ops % num_threads;
     int start_idx, end_idx;
-    std::thread threads[num_threads];
+    std::vector<std::thread> threads(NUM_THREADS);
 
     for (int i = 0; i < num_threads; i++) {
         if (i == 0) {
@@ -624,7 +624,7 @@ void compute_layer_derv_mean_var_fc_mt(
     const int n_batch = tot_ops / num_threads;
     const int rem_batch = tot_ops % num_threads;
     int start_idx, end_idx;
-    std::thread threads[num_threads];
+    std::vector<std::thread> threads(NUM_THREADS);
     for (int i = 0; i < num_threads; i++) {
         if (i == 0) {
             start_idx = n_batch * i;
@@ -725,7 +725,7 @@ void compute_cov_dz_fc_mt(std::vector<float> &ma, std::vector<float> &J,
     const int n_batch = tot_ops / num_threads;
     const int rem_batch = tot_ops % num_threads;
     int start_idx, end_idx;
-    std::thread threads[num_threads];
+    std::vector<std::thread> threads(NUM_THREADS);
 
     for (int i = 0; i < num_threads; i++) {
         if (i == 0) {
@@ -786,7 +786,7 @@ void compute_cov_last_current_layers_mt(
     const int n_batch = tot_ops / num_threads;
     const int rem_batch = tot_ops % num_threads;
     int start_idx, end_idx;
-    std::thread threads[num_threads];
+    std::vector<std::thread> threads(NUM_THREADS);
     for (int i = 0; i < num_threads; i++) {
         if (i == 0) {
             start_idx = n_batch * i;
@@ -836,7 +836,7 @@ void compute_cov_last_layer_minus_1_fc_mt(std::vector<float> &mw,
     const int n_batch = tot_ops / num_threads;
     const int rem_batch = tot_ops % num_threads;
     int start_idx, end_idx;
-    std::thread threads[num_threads];
+    std::vector<std::thread> threads(NUM_THREADS);
     for (int i = 0; i < num_threads; i++) {
         if (i == 0) {
             start_idx = n_batch * i;
@@ -875,7 +875,7 @@ void copy_derv_mt(std::vector<float> &md_layer_m, int ni, int no, int nn, int B,
     const int n_batch = tot_ops / num_threads;
     const int rem_batch = tot_ops % num_threads;
     int start_idx, end_idx;
-    std::thread threads[num_threads];
+    std::vector<std::thread> threads(NUM_THREADS);
     for (int i = 0; i < num_threads; i++) {
         if (i == 0) {
             start_idx = n_batch * i;
@@ -917,7 +917,7 @@ void sum_derv_mt(std::vector<float> &d_layer_m, int ni, int no, int B,
     const int n_batch = tot_ops / num_threads;
     const int rem_batch = tot_ops % num_threads;
     int start_idx, end_idx;
-    std::thread threads[num_threads];
+    std::vector<std::thread> threads(NUM_THREADS);
     for (int i = 0; i < num_threads; i++) {
         if (i == 0) {
             start_idx = n_batch * i;
@@ -957,7 +957,7 @@ void tanh_derv_mt(std::vector<float> &ma, std::vector<float> &Sa,
     const int n_batch = n / num_threads;
     const int rem_batch = n % num_threads;
     int start_idx, end_idx;
-    std::thread threads[num_threads];
+    std::vector<std::thread> threads(NUM_THREADS);
 
     for (int i = 0; i < num_threads; i++) {
         if (i == 0) {
@@ -995,7 +995,7 @@ void sigmoid_derv_mt(std::vector<float> &ma, std::vector<float> &Sa,
     const int n_batch = n / num_threads;
     const int rem_batch = n % num_threads;
     int start_idx, end_idx;
-    std::thread threads[num_threads];
+    std::vector<std::thread> threads(NUM_THREADS);
 
     for (int i = 0; i < num_threads; i++) {
         if (i == 0) {
@@ -1033,7 +1033,7 @@ void relu_derv_mt(std::vector<float> &mz, int z_pos, int n,
     const int n_batch = n / num_threads;
     const int rem_batch = n % num_threads;
     int start_idx, end_idx;
-    std::thread threads[num_threads];
+    std::vector<std::thread> threads(NUM_THREADS);
 
     for (int i = 0; i < num_threads; i++) {
         if (i == 0) {
@@ -1065,7 +1065,7 @@ void no_act_derv_mt(int z_pos, int n, unsigned int num_threads,
     const int n_batch = n / num_threads;
     const int rem_batch = n % num_threads;
     int start_idx, end_idx;
-    std::thread threads[num_threads];
+    std::vector<std::thread> threads(NUM_THREADS);
 
     for (int i = 0; i < num_threads; i++) {
         if (i == 0) {
diff --git a/src/lstm_layer.cpp b/src/lstm_layer.cpp
index f41f898fd..7158e37af 100644
--- a/src/lstm_layer.cpp
+++ b/src/lstm_layer.cpp
@@ -362,7 +362,7 @@ void lstm_cov_input_cell_states_mp(
     const int n_batch = tot_ops / NUM_THREADS;
     const int rem_batch = tot_ops % NUM_THREADS;
     int start_idx, end_idx;
-    std::thread threads[NUM_THREADS];
+    std::vector<std::thread> threads(NUM_THREADS);
 
     for (int i = 0; i < NUM_THREADS; i++) {
         get_multithread_indices(i, n_batch, rem_batch, start_idx, end_idx);
@@ -417,7 +417,7 @@ void lstm_cell_state_mean_var_mp(
     const int n_batch = tot_ops / NUM_THREADS;
     const int rem_batch = tot_ops % NUM_THREADS;
     int start_idx, end_idx;
-    std::thread threads[NUM_THREADS];
+    std::vector<std::thread> threads(NUM_THREADS);
 
     for (int i = 0; i < NUM_THREADS; i++) {
         get_multithread_indices(i, n_batch, rem_batch, start_idx, end_idx);
@@ -482,7 +482,7 @@ void lstm_cov_output_tanh_cell_states_mp(
     const int n_batch = tot_ops / NUM_THREADS;
     const int rem_batch = tot_ops % NUM_THREADS;
     int start_idx, end_idx;
-    std::thread threads[NUM_THREADS];
+    std::vector<std::thread> threads(NUM_THREADS);
 
     for (int i = 0; i < NUM_THREADS; i++) {
         get_multithread_indices(i, n_batch, rem_batch, start_idx, end_idx);
@@ -534,7 +534,7 @@ void lstm_hidden_state_mean_var_mp(
     const int n_batch = tot_ops / NUM_THREADS;
     const int rem_batch = tot_ops % NUM_THREADS;
     int start_idx, end_idx;
-    std::thread threads[NUM_THREADS];
+    std::vector<std::thread> threads(NUM_THREADS);
 
     for (int i = 0; i < NUM_THREADS; i++) {
         get_multithread_indices(i, n_batch, rem_batch, start_idx, end_idx);
@@ -585,7 +585,7 @@ void lstm_cat_activations_and_prev_states_mp(std::vector<float> &a,
     const int n_batch = tot_ops / NUM_THREADS;
     const int rem_batch = tot_ops % NUM_THREADS;
     int start_idx, end_idx;
-    std::thread threads[NUM_THREADS];
+    std::vector<std::thread> threads(NUM_THREADS);
 
     for (int i = 0; i < NUM_THREADS; i++) {
         get_multithread_indices(i, n_batch, rem_batch, start_idx, end_idx);
@@ -676,7 +676,7 @@ void lstm_delta_mean_var_z_mp(
     const int n_batch = tot_ops / NUM_THREADS;
     const int rem_batch = tot_ops % NUM_THREADS;
     int start_idx, end_idx;
-    std::thread threads[NUM_THREADS];
+    std::vector<std::thread> threads(NUM_THREADS);
 
     for (int i = 0; i < NUM_THREADS; i++) {
         get_multithread_indices(i, n_batch, rem_batch, start_idx, end_idx);
@@ -880,7 +880,7 @@ void lstm_delta_mean_var_w_mp(
     const int n_batch = tot_ops / NUM_THREADS;
     const int rem_batch = tot_ops % NUM_THREADS;
     int start_idx, end_idx;
-    std::thread threads[NUM_THREADS];
+    std::vector<std::thread> threads(NUM_THREADS);
 
     for (int i = 0; i < NUM_THREADS; i++) {
         get_multithread_indices(i, n_batch, rem_batch, start_idx, end_idx);
@@ -986,7 +986,7 @@ void lstm_delta_mean_var_b_mp(
     const int n_batch = tot_ops / NUM_THREADS;
     const int rem_batch = tot_ops % NUM_THREADS;
     int start_idx, end_idx;
-    std::thread threads[NUM_THREADS];
+    std::vector<std::thread> threads(NUM_THREADS);
 
     for (int i = 0; i < NUM_THREADS; i++) {
         get_multithread_indices(i, n_batch, rem_batch, start_idx, end_idx);


Reviews:
user: login: van-dai-vuong
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-03T19:43:06Z
user: login: lhnguyen102
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-06T19:17:48Z
user: login: miquelflorensa
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-06T19:59:29Z
user: login: lhnguyen102
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-06T21:06:27Z
user: login: lhnguyen102
state: APPROVED
author_association: OWNER
submitted_at: 2025-04-06T21:20:03Z

Review comments:
diff_hunk: @@ -106,6 +106,48 @@ jobs:
           name: macos-wheels-${{ matrix.python_version }}
           path: ./wheelhouse/*.whl
 
+  build_windows:
+    name: Windows Build (CPU Only)
+    runs-on: windows-latest
+    strategy:
+      matrix:
+        include:
+          - python_version: "3.10"
+            cibw_build: "cp310-win_amd64"
+          - python_version: "3.11"
+            cibw_build: "cp311-win_amd64"
+          - python_version: "3.12"
+            cibw_build: "cp312-win_amd64"
+    if: ${{ github.event.pull_request.draft == false }}
+    steps:
+      - uses: actions/checkout@v4
+        with:
+          submodules: recursive
+
+      - name: Set up Python ${{ matrix.python_version }}
+        uses: actions/setup-python@v5
+        with:
+          python-version: ${{ matrix.python_version }}
+
+      - name: Install cibuildwheel and NumPy <2.0
+        run: |
+          python -m pip install cibuildwheel==2.21.3
+          python -m pip install "numpy<2.0"
+
+      - name: Build wheels (CPU Only)
+        run: |
+          python -m cibuildwheel --output-dir wheelhouse
+        env:
+          CIBW_BUILD: ${{ matrix.cibw_build }}
+          CIBW_ARCHS_WINDOWS: auto64
+          CIBW_BUILD_VERBOSITY: 1
+          CIBW_ENVIRONMENT: "FORCE_CPU_ONLY=1 CMAKE_ARGS='-DBUILD_TESTING=OFF'"
path: .github/workflows/build_wheels.yml
user: login: lhnguyen102
body: This is a clever way to make it work with minimum code changes
created_at: 2025-04-06T19:17:28Z
author_association: OWNER
diff_hunk: @@ -198,33 +198,33 @@ target_link_libraries(cutagi PRIVATE cutagi_lib)
 ################################################################################
 # TESTS
 ################################################################################
-enable_testing()
-
-# Glob the test sources
-file(GLOB TEST_SOURCES "test/cpp_unit/*.cpp")
-
-# Add main.cpp separately to specify the entry point
-set(TEST_MAIN "test/cpp_unit/main.cpp")
-
-# Create an executable for the tests
-add_executable(run_tests ${TEST_MAIN} ${TEST_SOURCES})
-
-# Set include directories for the test executable
-target_include_directories(run_tests PRIVATE
-  ${pybind11_INCLUDE_DIRS}
-  ${CMAKE_CURRENT_SOURCE_DIR}/test/cpp_unit
-)
-
-# Link the test executable with the required libraries and cutagi_lib
-target_link_libraries(run_tests PRIVATE
-  ${Python_INCLUDE_DIRS}
-  pybind11::embed
-  gtest_main
-  cutagi_lib
-)
-
-# Discover GoogleTest tests
-include(GoogleTest)
-gtest_discover_tests(run_tests)
-
-
+if(BUILD_TESTING)
path: CMakeLists.txt
user: login: lhnguyen102
body: Any reason why we need to do this?
created_at: 2025-04-06T19:17:44Z
author_association: OWNER
diff_hunk: @@ -198,33 +198,33 @@ target_link_libraries(cutagi PRIVATE cutagi_lib)
 ################################################################################
 # TESTS
 ################################################################################
-enable_testing()
-
-# Glob the test sources
-file(GLOB TEST_SOURCES "test/cpp_unit/*.cpp")
-
-# Add main.cpp separately to specify the entry point
-set(TEST_MAIN "test/cpp_unit/main.cpp")
-
-# Create an executable for the tests
-add_executable(run_tests ${TEST_MAIN} ${TEST_SOURCES})
-
-# Set include directories for the test executable
-target_include_directories(run_tests PRIVATE
-  ${pybind11_INCLUDE_DIRS}
-  ${CMAKE_CURRENT_SOURCE_DIR}/test/cpp_unit
-)
-
-# Link the test executable with the required libraries and cutagi_lib
-target_link_libraries(run_tests PRIVATE
-  ${Python_INCLUDE_DIRS}
-  pybind11::embed
-  gtest_main
-  cutagi_lib
-)
-
-# Discover GoogleTest tests
-include(GoogleTest)
-gtest_discover_tests(run_tests)
-
-
+if(BUILD_TESTING)
path: CMakeLists.txt
user: login: miquelflorensa
body: I am not really sure why Windows is not able to compile the tests correctly. Researching it seems that there are some flags in the built that works for gcc (`-pthread`, `-flto=auto`) but they are invalid in MSVC.
created_at: 2025-04-06T19:59:29Z
author_association: COLLABORATOR
diff_hunk: @@ -198,33 +198,33 @@ target_link_libraries(cutagi PRIVATE cutagi_lib)
 ################################################################################
 # TESTS
 ################################################################################
-enable_testing()
-
-# Glob the test sources
-file(GLOB TEST_SOURCES "test/cpp_unit/*.cpp")
-
-# Add main.cpp separately to specify the entry point
-set(TEST_MAIN "test/cpp_unit/main.cpp")
-
-# Create an executable for the tests
-add_executable(run_tests ${TEST_MAIN} ${TEST_SOURCES})
-
-# Set include directories for the test executable
-target_include_directories(run_tests PRIVATE
-  ${pybind11_INCLUDE_DIRS}
-  ${CMAKE_CURRENT_SOURCE_DIR}/test/cpp_unit
-)
-
-# Link the test executable with the required libraries and cutagi_lib
-target_link_libraries(run_tests PRIVATE
-  ${Python_INCLUDE_DIRS}
-  pybind11::embed
-  gtest_main
-  cutagi_lib
-)
-
-# Discover GoogleTest tests
-include(GoogleTest)
-gtest_discover_tests(run_tests)
-
-
+if(BUILD_TESTING)
path: CMakeLists.txt
user: login: lhnguyen102
body: those two wont work with windows. I am just wondering why it does not work for google unittest but our code works just fine when compiling because our code use pthread as welll 
created_at: 2025-04-06T21:06:23Z
author_association: OWNER

PR comments:
user: login: van-dai-vuong
created_at: 2025-04-04T13:59:03Z
body: @lhnguyen102 On our folk branches, we have tried to make a release using Github Action, the wheels for Windows are created successfully. We have also tested to use that wheel (.whl) to `pip install .whl` on our Windows server, pytagi is installed successfully.
author_association: COLLABORATOR
user: login: lhnguyen102
created_at: 2025-04-05T21:35:57Z
body: @miquelflorensa Did you test  CUDA on Windows? 
author_association: OWNER
user: login: miquelflorensa
created_at: 2025-04-06T18:36:33Z
body: @lhnguyen102 I tried to make it run with CUDA in Windows Server 2022 but have not been able so far. I haven't tried on any other Windows version.
author_association: COLLABORATOR
user: login: lhnguyen102
created_at: 2025-04-06T19:16:55Z
body: @miquelflorensa no worries
author_association: OWNER
user: login: lhnguyen102
created_at: 2025-04-06T19:19:50Z
body: > We have also tested to use that wheel (.whl) to pip install .whl on our Windows server, pytagi is installed successfully.

@miquelflorensa Did you build python wheel manually using cibuildwheel or you download it from output of Github action for testing?
author_association: OWNER
user: login: van-dai-vuong
created_at: 2025-04-06T19:25:07Z
body: > > We have also tested to use that wheel (.whl) to pip install .whl on our Windows server, pytagi is installed successfully.
> 
> @miquelflorensa Did you build python wheel manually using cibuildwheel or you download it from output of Github action for testing?

We downloaded and used the one from Github action to test
author_association: COLLABORATOR
user: login: lhnguyen102
created_at: 2025-04-06T21:23:58Z
body: @miquelflorensa you can merge your PR. I'll rebase my PR right after
author_association: OWNER