diff --git a/.github/actions/init/action.yaml b/.github/actions/init/action.yaml index c974c67a..c8268f2d 100644 --- a/.github/actions/init/action.yaml +++ b/.github/actions/init/action.yaml @@ -23,19 +23,15 @@ inputs: runs: using: "composite" steps: - - name: "Ensure doxygen" - if: inputs.doc-tools + - name: "Ensure doxygen (Linux)" + if: inputs.doc-tools && runner.os == 'Linux' run: sudo apt-get install graphviz doxygen -y - # latest doxygen (untested) - # run: | - # if ! command -v doxygen &> /dev/null - # then - # echo "doxygen wasn't found... installing now..." - # mkdir -p ~/.local/bin - # curl -sSL https://www.doxygen.nl/files/doxygen-1.9.4.linux.bin.tar.gz | \ - # tar -xzvf - --strip-components=1 -C ~/.local/bin bin/doxygen; - # fi + shell: bash + - name: "Ensure doxygen (Windows)" + if: inputs.doc-tools && runner.os == 'Windows' + run: | + choco install doxygen.install graphviz --yes shell: bash - name: "Ensure poetry" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 64851f6a..37cf2b6e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,10 +25,11 @@ permissions: jobs: test: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} strategy: matrix: python-version: ["3.9", "3.10", "3.11", "3.12"] + os: [ubuntu-latest, windows-latest] steps: - name: "Checkout Code" @@ -46,6 +47,7 @@ jobs: run: | mkdir -p docs/doxygen poetry run pytest --emoji -v -s --md $GITHUB_STEP_SUMMARY + shell: bash qa: runs-on: ubuntu-latest diff --git a/doxysphinx/utils/files.py b/doxysphinx/utils/files.py index 4f4a4d32..8416e610 100644 --- a/doxysphinx/utils/files.py +++ b/doxysphinx/utils/files.py @@ -137,8 +137,13 @@ def copy_if_different( source_file = file target_file = target_dir / source_file.relative_to(source_dir) target_file.parent.mkdir(parents=True, exist_ok=True) - shutil.copy(source_file, target_file) - result.append(target_file) + if os.name == "nt": + # Use extended-length paths on Windows to support long file names + # Apply \\?\ prefix only to the shutil operation, not to pathlib operations + shutil.copy(rf"\\?\{source_file}", rf"\\?\{target_file}") + else: + shutil.copy(source_file, target_file) + result.append(target_file) # Return the original path without prefix return result diff --git a/tests/test_commands.py b/tests/test_commands.py index 5d08d4b8..4db518a7 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -7,7 +7,9 @@ # - Markus Braun, :em engineering methods AG (contracted by Robert Bosch GmbH) # ===================================================================================== +import os from pathlib import Path +import shutil from click.testing import CliRunner @@ -39,9 +41,75 @@ def test_build_is_working_as_expected(): print("Build had errors - std output stream:") print(result.stdout) assert result.exit_code == 0 - print("test2") assert (repo_root / ".build/html/docs/doxygen/demo/html/doxygen.css").exists() - print("test3") + + +def test_longpath_build(): + """Test building with long paths""" + runner = CliRunner() + repo_root = path_resolve(Path()) + + # Copy our demo directory to a very long path + # The path needs to be over 255 characters long to trigger issues on Windows + # Windows has a max path length of 255 characters by default + # (see https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation) + long_dir_name = "a" * 250 # Single very long directory name + long_path = repo_root / long_dir_name + + # Test if we can create the long path at all + if os.name == "nt": + # On Windows, use extended-length path support + try: + # Use \\?\ prefix for the actual copy operation + shutil.copytree(rf"\\?\{repo_root}", rf"\\?\{long_path}", dirs_exist_ok=True) + except OSError as e: + raise AssertionError( + f"Long path copy failed on Windows - this indicates the tool won't work with long paths: {e}") + else: + shutil.copytree(repo_root, long_path, dirs_exist_ok=True) + + # Verify the copy worked by checking for key files + # On Windows with long paths, use os.path.exists with \\?\ prefix for verification + if os.name == "nt": + assert os.path.exists(rf"\\?\{long_path}"), f"Long path directory was not created: {long_path}" + assert os.path.exists(rf"\\?\{long_path}\pyproject.toml"), f"pyproject.toml not found in long path: {long_path}" + else: + assert long_path.exists(), f"Directory was not created: {long_path}" + assert (long_path / "pyproject.toml").exists(), f"pyproject.toml not found: {long_path}" + + sphinx_source = long_path + sphinx_output = long_path / ".build/html" + doxyfile = long_path / "demo" / "demo.doxyfile" + + result = runner.invoke( + cli, + [ + "--verbosity=DEBUG", + "build", + "--doxygen_cwd", + str(long_path), + str(sphinx_source), + str(sphinx_output), + str(doxyfile), + ], + ) + if result.exit_code != 0: + print("Build had errors - std output stream:") + print(result.stdout) + assert result.exit_code == 0 + # Check the final output file + css_file_path = long_path / ".build/html/docs/doxygen/demo/html/doxygen.css" + if os.name == "nt": + assert os.path.exists(rf"\\?\{css_file_path}"), f"Output CSS file missing: {css_file_path}" + else: + assert css_file_path.exists(), f"Output CSS file missing: {css_file_path}" + + # Clean up - only if the directory was actually created + if long_path.exists(): + if os.name == "nt": + shutil.rmtree(rf"\\?\{long_path}") + else: + shutil.rmtree(long_path) def test_worker_limiting(): @@ -85,9 +153,7 @@ def test_worker_limiting(): print("Build had errors - std output stream:") print(result.stdout) assert result.exit_code == 0 - print("test2") assert (repo_root / ".build/html/docs/doxygen/demo/html/doxygen.css").exists() - print("test3") assert f"running in parallel with limit of {worker_limit} workers" in result.stdout