From bc3db60911f3959ad221bfb240a4ee5f7619c8a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Goll?= Date: Tue, 13 May 2025 13:38:33 +0200 Subject: [PATCH 01/16] ENH: Add option to run installation in non-interactive mode (no popups/prompting) --- .../InstallLogic.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/MultiverSeg/SegmentEditorMultiverSegLib/InstallLogic.py b/MultiverSeg/SegmentEditorMultiverSegLib/InstallLogic.py index dfc71c6..1fb7715 100644 --- a/MultiverSeg/SegmentEditorMultiverSegLib/InstallLogic.py +++ b/MultiverSeg/SegmentEditorMultiverSegLib/InstallLogic.py @@ -14,6 +14,8 @@ class InstallLogic: SCRIBBLEPROMPT_FILE_NAME = "ScribblePrompt_unet_v1_nf192_res128.pt" SCRIBBLEPROMPT_DOWNLOAD_URL = "https://www.dropbox.com/scl/fi/pnw88n05irnv5z1snlklr/ScribblePrompt_unet_v1_nf192_res128.pt?rlkey=dr8xvkf0wj2r082h1zzpcmz5o&dl=1" + INTERACTIVE_MODE = True # Can the user interact with the window + @classmethod def downloadCheckpointsIfNeeded(cls): try: @@ -32,7 +34,8 @@ def downloadMultiverSegCheckpointIfNeeded(cls): modelPath = cls.CKPT_DIR.joinpath("MultiverSeg_v0_nf256_res128.pt") if not modelPath.is_file(): - if slicer.util.confirmOkCancelDisplay("The MultiverSeg model is required. Confirm to download (74MB)."): + if (not cls.INTERACTIVE_MODE) or slicer.util.confirmOkCancelDisplay( + "The MultiverSeg model is required. Confirm to download (74MB)."): cls._downloadModel(cls.MULTIVERSEG_FILE_NAME, cls.MULTIVERSEG_DOWNLOAD_URL) return True # Model downloaded correctly @@ -45,7 +48,7 @@ def downloadScribblePromptCheckpointIfNeeded(cls): modelPath = cls.CKPT_DIR.joinpath(cls.SCRIBBLEPROMPT_FILE_NAME) if not modelPath.is_file(): - if slicer.util.confirmOkCancelDisplay("The ScribblePrompt model is required. Confirm to download (16MB)."): + if (not cls.INTERACTIVE_MODE) or slicer.util.confirmOkCancelDisplay("The ScribblePrompt model is required. Confirm to download (16MB)."): cls._downloadModel(cls.SCRIBBLEPROMPT_FILE_NAME, cls.SCRIBBLEPROMPT_DOWNLOAD_URL) @@ -89,6 +92,7 @@ def reportProgress(logic, msg, level=None): class DependenciesLogic: + INTERACTIVE_MODE = True # Can the user interact with the window # Install dependencies to torch and multiverseg if not already installed @classmethod @@ -114,7 +118,7 @@ def installPyTorchExtensionIfNeeded(cls): import PyTorchUtils # noqa return True except ModuleNotFoundError: - ret = slicer.util.confirmOkCancelDisplay("""This module requires PyTorch extension. Would you like to install it? + ret = (not cls.INTERACTIVE_MODE) or slicer.util.confirmOkCancelDisplay("""This module requires PyTorch extension. Would you like to install it? Slicer will need to be restarted before continuing the install.""", "PyTorch extension not found.") if ret: @@ -122,9 +126,10 @@ def installPyTorchExtensionIfNeeded(cls): return False # Need restart or not installed # Install the PyTorch Utils extension - @staticmethod - def installPyTorchExtension(): + @classmethod + def installPyTorchExtension(cls): extensionManager = slicer.app.extensionsManagerModel() + extensionManager.setInteractive(cls.INTERACTIVE_MODE) extName = "PyTorch" if extensionManager.isExtensionInstalled(extName): return @@ -140,7 +145,7 @@ def installMultiverSegIfNeeded(cls): import multiverseg return True except ModuleNotFoundError: - ret = slicer.util.confirmOkCancelDisplay( + ret = (not cls.INTERACTIVE_MODE) or slicer.util.confirmOkCancelDisplay( "This module requires the MultiverSeg python package. Would you like to install it?", "MultiverSeg package not found.") if ret: @@ -169,7 +174,7 @@ def installTorchIfNeeded(cls): def installTorch(cls): import PyTorchUtils torchLogic = PyTorchUtils.PyTorchUtilsLogic() - res = torchLogic.installTorch(askConfirmation=True) + res = torchLogic.installTorch(askConfirmation=cls.INTERACTIVE_MODE, torchvisionVersionRequirement=">=0.10") if res is None: raise RuntimeError("Failed to install torch and torchvision. " From 5a310851d9b5dc29b0f0838b4e6251ce430df91c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Goll?= Date: Tue, 13 May 2025 13:46:35 +0200 Subject: [PATCH 02/16] CI: Create action for slicer installation --- .github/actions/install-slicer/action.yml | 25 +++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/actions/install-slicer/action.yml diff --git a/.github/actions/install-slicer/action.yml b/.github/actions/install-slicer/action.yml new file mode 100644 index 0000000..b9ebbf7 --- /dev/null +++ b/.github/actions/install-slicer/action.yml @@ -0,0 +1,25 @@ +name: install-slicer +author: Sebastien Goll (Kitware SAS) +description: Install 3D Slicer +inputs: + version: + description: Version of Slicer available at https://community.chocolatey.org/packages/3dslicer#versionhistory + default: '' +outputs: + slicer_folder: + description: Installation folder for Slicer + value: ${{ steps.slicer_folder.outputs.slicer_folder }} + +runs: + using: composite + steps: + - if: ${{runner.os == 'Windows'}} + run: choco install 3dslicer -y --version=${{ inputs.version }} --allowdowngrade + shell: bash + - if: ${{runner.os == 'Windows'}} + id: slicer_folder + shell: bash + run: | + folder=$(find "C:/ProgramData/slicer.org" -maxdepth 1 -type d -name Slicer* -exec stat --format "%Y %n" {} + |sort -nr | head -n1 | cut -d' ' -f2-) + echo "slicer_folder=$folder" >> $GITHUB_OUTPUT + From 3e2d8c3b38edd4c26d63e981d925d329495d9de7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Goll?= Date: Tue, 13 May 2025 13:47:06 +0200 Subject: [PATCH 03/16] CI: Create action for running python script in slicer --- .../slicer-run-python-script/action.yml | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/actions/slicer-run-python-script/action.yml diff --git a/.github/actions/slicer-run-python-script/action.yml b/.github/actions/slicer-run-python-script/action.yml new file mode 100644 index 0000000..1b22455 --- /dev/null +++ b/.github/actions/slicer-run-python-script/action.yml @@ -0,0 +1,21 @@ +name: slicer-run-python-script +author: Sebastien Goll (Kitware SAS) +description: Run a python script through Slicer +inputs: + slicer_dir: + description: Slicer directory + required: true + script: + description: Path to python to run + required: true + additional_arguments: + description: Additional arguments for Slicer executable + default: '' +runs: + using: composite + steps: + - run: | + "$SLICER_EXE" --python-script "${{ inputs.script }}" ${{ inputs.additional_arguments }} + shell: bash + env: + SLICER_EXE: ${{inputs.slicer_dir}}/Slicer.exe \ No newline at end of file From 29770cb470626e56e8f4118b0e0ed7ae7c4ac1d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Goll?= Date: Tue, 13 May 2025 13:47:52 +0200 Subject: [PATCH 04/16] CI: Add installation scripts for multiverseg --- .github/python_scripts/install_python_dependencies.py | 11 +++++++++++ .github/python_scripts/install_pytorch_module.py | 8 ++++++++ 2 files changed, 19 insertions(+) create mode 100644 .github/python_scripts/install_python_dependencies.py create mode 100644 .github/python_scripts/install_pytorch_module.py diff --git a/.github/python_scripts/install_python_dependencies.py b/.github/python_scripts/install_python_dependencies.py new file mode 100644 index 0000000..436d3e9 --- /dev/null +++ b/.github/python_scripts/install_python_dependencies.py @@ -0,0 +1,11 @@ +import slicer.util +from MultiverSeg.SegmentEditorMultiverSegLib import InstallLogic, DependenciesLogic + +if __name__ == '__main__': + DependenciesLogic.INTERACTIVE_MODE = False + InstallLogic.INTERACTIVE_MODE = False + + DependenciesLogic.installTorchIfNeeded() + DependenciesLogic.installMultiverSegIfNeeded() + InstallLogic.downloadCheckpointsIfNeeded() + slicer.util.quit() diff --git a/.github/python_scripts/install_pytorch_module.py b/.github/python_scripts/install_pytorch_module.py new file mode 100644 index 0000000..02da728 --- /dev/null +++ b/.github/python_scripts/install_pytorch_module.py @@ -0,0 +1,8 @@ +import slicer.util + +from MultiverSeg.SegmentEditorMultiverSegLib import DependenciesLogic + +if __name__ == '__main__': + DependenciesLogic.INTERACTIVE_MODE = False + DependenciesLogic.installPyTorchExtensionIfNeeded() + slicer.util.quit() From 214623265de85afd72172c9462977b33c310ff69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Goll?= Date: Tue, 13 May 2025 13:55:55 +0200 Subject: [PATCH 05/16] CI: Add action and script to run tests --- .../slicer-run-python-tests/action.yml | 29 +++++++++++++++++++ .github/python_scripts/run_tests.py | 17 +++++++++++ 2 files changed, 46 insertions(+) create mode 100644 .github/actions/slicer-run-python-tests/action.yml create mode 100644 .github/python_scripts/run_tests.py diff --git a/.github/actions/slicer-run-python-tests/action.yml b/.github/actions/slicer-run-python-tests/action.yml new file mode 100644 index 0000000..e524408 --- /dev/null +++ b/.github/actions/slicer-run-python-tests/action.yml @@ -0,0 +1,29 @@ +name: slicer-run-python-tests +author: Sebastien Goll (Kitware SAS) +description: Run python tests in Slicer + +inputs: + slicer_dir: + description: Slicer directory + required: true + additional_arguments: + description: Additional arguments for Slicer executable + default: '' + tests_root_path: + description: Root path to search for tests + required: true + tests_name_pattern: + description: Pattern used to find test files + required: true + module_paths: + description: Paths to the tested modules + required: true + +runs: + using: composite + steps: + - uses: ./.github/actions/slicer-run-python-script + with: + script: ./.github/python_scripts/run_tests.py + slicer_dir: ${{ inputs.slicer_dir }} + additional_arguments: --additional-module-paths ${{ inputs.module_paths }} ${{ inputs.additional_arguments }} ${{ inputs.tests_root_path }} ${{ inputs.tests_name_pattern }} diff --git a/.github/python_scripts/run_tests.py b/.github/python_scripts/run_tests.py new file mode 100644 index 0000000..0d84d88 --- /dev/null +++ b/.github/python_scripts/run_tests.py @@ -0,0 +1,17 @@ +import sys +from pathlib import Path + +import slicer.testing + +if __name__ == "__main__": + + if len(sys.argv) < 3: + raise AttributeError(f"run_tests.py requires 2 arguments, found {sys.argv[1:]}") + + root = Path(sys.argv[1]) + files = list(root.glob(sys.argv[2])) + + print(f"Found {len(files)} test file(s).") + + for file in files: + slicer.testing.runUnitTest(file.parent.as_posix(), file.stem) From 93024acfdb8c05e0646844ae8dc794034c99adb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Goll?= Date: Tue, 13 May 2025 13:58:13 +0200 Subject: [PATCH 06/16] CI: Update workflow to use actions --- .github/workflows/windows-testing.yml | 112 ++++++-------------------- 1 file changed, 26 insertions(+), 86 deletions(-) diff --git a/.github/workflows/windows-testing.yml b/.github/workflows/windows-testing.yml index 7f145a5..a5fb506 100644 --- a/.github/workflows/windows-testing.yml +++ b/.github/workflows/windows-testing.yml @@ -11,90 +11,30 @@ on: jobs: Tests-Windows: runs-on: windows-latest - outputs: - install_path: ${{ steps.slicerLocation.outputs.install_path }} - python_path: ${{ steps.pythonLocation.outputs.python_path }} steps: - - name: Install Slicer - run: choco install 3dslicer -y - - name: Get slicer executable - id: slicerLocation - run: | - $folder = Get-ChildItem -Path "C:\ProgramData\slicer.org" -Filter "Slicer.exe" -Recurse -ErrorAction SilentlyContinue -Force | - Select-Object -First 1 - - if ($folder) { - $installPath = $folder.FullName - Write-Host "Found Slicer at $installPath" - # Set the output - "install_path=$installPath" >> $env:GITHUB_OUTPUT - } else { - Write-Host "Slicer not found." - exit 1 - } - - name: Get python env - id: pythonLocation - run: | - $pythonPath = Resolve-Path "${{ steps.slicerLocation.outputs.install_path }}/../bin/PythonSlicer.exe" - echo "$pythonPath" - "python_path=$pythonPath" >> $env:GITHUB_OUTPUT - - name: Install SlicerPytorch module - id: slicerpytorch - run: | - $path = "${{ steps.slicerLocation.outputs.install_path }}" - $process = Start-Process -FilePath "$path" -ArgumentList "--testing", "--no-splash", "--no-main-window", "--python-code `"slicer.app.extensionsManagerModel().installExtensionFromServer('PyTorch')`"" -Wait -RedirectStandardOutput "log_std.txt" -RedirectStandardError "log_err.txt" -PassThru - echo "Standard output:" - Get-Content log_std.txt - echo "Error output:" - Get-Content log_err.txt - $exitCode = $process.ExitCode - echo "Exit code: $exitCode" - $folder = Get-ChildItem -Path "${{ steps.slicerLocation.outputs.install_path }}/../slicer.org" -Include qt-*modules -Recurse | Select-Object -First 1 - $folder = $folder.FullName - echo "PyTorch module installed at $folder" - "slicerPyTorch=$folder" >> $env:GITHUB_OUTPUT - exit $exitCode - - name: Install pytorch - run: | - $path = "${{ steps.slicerLocation.outputs.install_path }}" - $pytorchmodule = "${{ steps.slicerpytorch.outputs.slicerPyTorch }}" - $process = Start-Process -FilePath "$path" -ArgumentList "--testing", "--no-splash", "--no-main-window","--additional-module-paths `"$pytorchmodule`"" ,"--python-code `"import PyTorchUtils;PyTorchUtils.PyTorchUtilsLogic().installTorch(askConfirmation=False,torchvisionVersionRequirement='>=0.20')`"" -Wait -RedirectStandardOutput "log_std.txt" -RedirectStandardError "log_err.txt" -PassThru - echo "Standard output:" - Get-Content log_std.txt - echo "Error output:" - Get-Content log_err.txt - $exitCode = $process.ExitCode - echo "Exit code: $exitCode" - exit $exitCode - - name: Install multiverseg - run: | - $path = "${{ steps.pythonLocation.outputs.python_path }}" - & $path -m pip install "git+https://github.com/halleewong/MultiverSeg.git" - - name: Check out repository code - uses: actions/checkout@v4 - - name: Download models - run: | - $path = "${{ steps.slicerLocation.outputs.install_path }}" - $modulepath= "${{ github.workspace }}/MultiverSeg" - $process = Start-Process -FilePath "$path" -ArgumentList "--testing", "--no-splash", "--no-main-window","--additional-module-paths `"$modulepath`"" ,"--python-code `"from SegmentEditorMultiverSegLib import InstallLogic as i;i._downloadModel(i.MULTIVERSEG_FILE_NAME, i.MULTIVERSEG_DOWNLOAD_URL);i._downloadModel(i.SCRIBBLEPROMPT_FILE_NAME, i.SCRIBBLEPROMPT_DOWNLOAD_URL)`"" -Wait -RedirectStandardOutput "log_std.txt" -RedirectStandardError "log_err.txt" -PassThru - - name: Test - run: | - $path = "${{ steps.slicerLocation.outputs.install_path }}" - $modulepath= "${{ github.workspace }}/MultiverSeg" - $pytorchmodule = "${{ steps.slicerpytorch.outputs.slicerPyTorch }}" - $testPath = "$modulepath/Testing/Python" - $testNames = Get-ChildItem ${{ github.workspace }} -Filter *TestCase.py -Recurse | ForEach-Object {$_.BaseName} - $exitCode = 0 - echo "Found tests:" - echo $testNames - foreach($testCase in $testNames){ - $process = Start-Process -FilePath "$path" -ArgumentList "--testing", "--no-splash", "--additional-module-paths `"$modulepath`" `"$pytorchmodule`"" ,"--python-code `"import slicer.testing; slicer.testing.runUnitTest(r'$testPath', '$testCase')`"" -Wait -RedirectStandardOutput "log_std.txt" -RedirectStandardError "log_err.txt" -PassThru - echo "Standard output:" - Get-Content log_std.txt - echo "Error output:" - Get-Content log_err.txt - $currentCode = $process.ExitCode - echo "Exit code: $currentCode" - $exitCode = ($currentCode , $exitCode | Measure-Object -Max).Maximum - } - exit $exitCode \ No newline at end of file + - uses: actions/checkout@v4 + name: Checkout + - uses: ./.github/actions/install-slicer + id: slicer_install + name: Install Slicer + - name: Install PyTorch module + uses: ./.github/actions/slicer-run-python-script + with: + script: './.github/python_scripts/install_pytorch_module.py' + slicer_dir: ${{ steps.slicer_install.outputs.slicer_folder }} + additional_arguments: --no-main-window --no-splash + - name: Install Python dependencies + uses: ./.github/actions/slicer-run-python-script + with: + script: './.github/python_scripts/install_python_dependencies.py' + slicer_dir: ${{ steps.slicer_install.outputs.slicer_folder }} + additional_arguments: --no-main-window --no-splash + - name: Run tests + uses: ./.github/actions/slicer-run-python-tests + with: + slicer_dir: ${{ steps.slicer_install.outputs.slicer_folder }} + additional_arguments: --testing --no-splash + tests_root_path: './MultiverSeg/Testing/Python' + tests_name_pattern: '*TestCase.py' + module_paths: './MultiverSeg' + From 7a3fe8a5217d7630cb6f85e5584f82fac7068899 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Goll?= Date: Tue, 13 May 2025 14:03:05 +0200 Subject: [PATCH 07/16] CI: Update run tests script location (cherry picked from commit 92f6d429c89a151c46ce2dc2648f82bbdd83356f) --- .github/actions/slicer-run-python-tests/action.yml | 2 +- .../slicer-run-python-tests}/run_tests.py | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename .github/{python_scripts => actions/slicer-run-python-tests}/run_tests.py (100%) diff --git a/.github/actions/slicer-run-python-tests/action.yml b/.github/actions/slicer-run-python-tests/action.yml index e524408..a664e3f 100644 --- a/.github/actions/slicer-run-python-tests/action.yml +++ b/.github/actions/slicer-run-python-tests/action.yml @@ -24,6 +24,6 @@ runs: steps: - uses: ./.github/actions/slicer-run-python-script with: - script: ./.github/python_scripts/run_tests.py + script: ${{ github.action_path }}/run_tests.py slicer_dir: ${{ inputs.slicer_dir }} additional_arguments: --additional-module-paths ${{ inputs.module_paths }} ${{ inputs.additional_arguments }} ${{ inputs.tests_root_path }} ${{ inputs.tests_name_pattern }} diff --git a/.github/python_scripts/run_tests.py b/.github/actions/slicer-run-python-tests/run_tests.py similarity index 100% rename from .github/python_scripts/run_tests.py rename to .github/actions/slicer-run-python-tests/run_tests.py From d049cf189ea4d7b8d17c0892fa58e05177a85b53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Goll?= Date: Tue, 13 May 2025 14:41:55 +0200 Subject: [PATCH 08/16] CI: Allow to manually run the workflow --- .github/workflows/windows-testing.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/windows-testing.yml b/.github/workflows/windows-testing.yml index a5fb506..4da4ffd 100644 --- a/.github/workflows/windows-testing.yml +++ b/.github/workflows/windows-testing.yml @@ -7,6 +7,8 @@ on: pull_request: branches: - main + workflow_dispatch: + jobs: Tests-Windows: @@ -38,3 +40,4 @@ jobs: tests_name_pattern: '*TestCase.py' module_paths: './MultiverSeg' + From 9caed097f7802c4445ec8a43041a9b16cf16269c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Goll?= Date: Tue, 13 May 2025 15:32:53 +0200 Subject: [PATCH 09/16] CI: Add extension install action (cherry picked from commit 8aee406421621ce2db4715e937bef93e2772a3cd) --- .../slicer-install-extensions/action.yml | 23 +++++++++++++++++++ .../install_extensions.py | 15 ++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 .github/actions/slicer-install-extensions/action.yml create mode 100644 .github/actions/slicer-install-extensions/install_extensions.py diff --git a/.github/actions/slicer-install-extensions/action.yml b/.github/actions/slicer-install-extensions/action.yml new file mode 100644 index 0000000..ba00a36 --- /dev/null +++ b/.github/actions/slicer-install-extensions/action.yml @@ -0,0 +1,23 @@ +name: slicer-install-extensions +author: Sebastien Goll (Kitware SAS) +description: Install extensions for Slicer + +inputs: + slicer_dir: + description: Slicer directory + required: true + additional_arguments: + description: Additional arguments for Slicer executable + default: '--no-main-window --no-splash' + extensions_name: + description: Extensions names to install + required: true + +runs: + using: composite + steps: + - uses: ./.github/actions/slicer-run-python-script + with: + script: ${{ github.action_path }}/install_extensions.py + slicer_dir: ${{ inputs.slicer_dir }} + additional_arguments: ${{ inputs.additional_arguments }} ${{ inputs.extensions_name }} diff --git a/.github/actions/slicer-install-extensions/install_extensions.py b/.github/actions/slicer-install-extensions/install_extensions.py new file mode 100644 index 0000000..ff954b3 --- /dev/null +++ b/.github/actions/slicer-install-extensions/install_extensions.py @@ -0,0 +1,15 @@ +import sys +import slicer.util + +if __name__ == '__main__': + + extension_names = sys.argv[1:] + print("Found extensions: ", extension_names) + + em = slicer.app.extensionsManagerModel() + em.setInteractive(False) + + for name in extension_names: + em.installExtensionFromServer(name, False) + + slicer.util.quit() From 0fd09f57b5bfc768aefe343599119a445f35c8de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Goll?= Date: Tue, 13 May 2025 15:54:32 +0200 Subject: [PATCH 10/16] CI: Add action to install python packages into slicer's python env (cherry picked from commit 068ef8bc18c0b21d0a62732771601e11c20b8b77) --- .../slicer-install-python-packages/action.yml | 22 +++++++++++++++++++ .../install_py_packages.py | 11 ++++++++++ 2 files changed, 33 insertions(+) create mode 100644 .github/actions/slicer-install-python-packages/action.yml create mode 100644 .github/actions/slicer-install-python-packages/install_py_packages.py diff --git a/.github/actions/slicer-install-python-packages/action.yml b/.github/actions/slicer-install-python-packages/action.yml new file mode 100644 index 0000000..813ece9 --- /dev/null +++ b/.github/actions/slicer-install-python-packages/action.yml @@ -0,0 +1,22 @@ +name: slicer-install-python-packages +author: Sebastien Goll (Kitware SAS) +description: Install python packages in Slicer's python environment + +inputs: + slicer_dir: + description: Slicer directory + required: true + additional_arguments: + description: Additional arguments for Slicer executable + default: '--no-main-window --no-splash' + package_names: + description: Packages to install + required: true +runs: + using: composite + steps: + - uses: ./.github/actions/slicer-run-python-script + with: + script: ${{ github.action_path }}/install_py_packages.py + slicer_dir: ${{ inputs.slicer_dir }} + additional_arguments: ${{ inputs.additional_arguments }} ${{ inputs.package_names }} diff --git a/.github/actions/slicer-install-python-packages/install_py_packages.py b/.github/actions/slicer-install-python-packages/install_py_packages.py new file mode 100644 index 0000000..f6914b5 --- /dev/null +++ b/.github/actions/slicer-install-python-packages/install_py_packages.py @@ -0,0 +1,11 @@ +import sys + +import slicer.util + +if __name__ == "__main__": + + print("Packages found:",sys.argv[1:]) + + for package in sys.argv[1:]: + slicer.util.pip_install(package) + slicer.util.quit() \ No newline at end of file From 41cb5e755c70d1b9f3e48e44c868d348a074be61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Goll?= Date: Thu, 15 May 2025 17:23:40 +0200 Subject: [PATCH 11/16] CI: Add slicer download script --- .github/actions/install-slicer/downloader.sh | 30 ++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/actions/install-slicer/downloader.sh diff --git a/.github/actions/install-slicer/downloader.sh b/.github/actions/install-slicer/downloader.sh new file mode 100644 index 0000000..05a1d87 --- /dev/null +++ b/.github/actions/install-slicer/downloader.sh @@ -0,0 +1,30 @@ +if [ $1 == "Windows" ]; then + PACK_OS="win" +elif [ $1 == "Linux" ]; then + PACK_OS="linux" +elif [ $1 == "macOS" ]; then + PACK_OS="macosx" +fi + +PACK_ARCH="amd64" +BASE_URL="https://slicer-packages.kitware.com/api/v1" + +APP_ID=$(curl -s "$BASE_URL/app?name=Slicer&limit=1" | jq -r '.[0]._id') +echo "Application id found for Slicer: $APP_ID" + +RELEASE_NAME=$(curl -s "$BASE_URL/app/$APP_ID/release?sort=meta.revision&sortdir=-1" | jq -r '.[0].lowerName') +echo "Realease name found: $RELEASE_NAME" + +PACK=$(curl -s "$BASE_URL/app/$APP_ID/package?release_id_or_name=$RELEASE_NAME&os=$PACK_OS&arch=$PACK_ARCH&limit=1" | jq '.[0]') +PACK_ID=$(jq -r '._id' <<< "$PACK") +PACK_NAME=$(jq -r '.name' <<< "$PACK") +echo "Name of the package found: $PACK_NAME ($PACK_ID)" + +mkdir -p installer + +HEADER_TMP=$(mktemp) +curl -# -D "$HEADER_TMP" "$BASE_URL/item/$PACK_ID/download" -o file +CONTENT_DISPOSITION=$(grep -i '^Content-Disposition:' "$HEADER_TMP") +FILENAME=$(echo "$CONTENT_DISPOSITION" | sed -n 's/.*filename="\(.*\)".*/\1/p') +mv file "installer/$FILENAME" + From c8611aef42bb5977c0dfab24455de88d8d3343b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Goll?= Date: Thu, 15 May 2025 17:24:05 +0200 Subject: [PATCH 12/16] CI: Add MacOS installation script --- .../actions/install-slicer/macos_installer.exp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .github/actions/install-slicer/macos_installer.exp diff --git a/.github/actions/install-slicer/macos_installer.exp b/.github/actions/install-slicer/macos_installer.exp new file mode 100644 index 0000000..3e09c1c --- /dev/null +++ b/.github/actions/install-slicer/macos_installer.exp @@ -0,0 +1,17 @@ +#!/usr/bin/expect -f + +set timeout 10 +set dmg_file [lindex $argv 0] + +spawn hdiutil attach "$dmg_file" + +sleep 2 +send "q\r" + +expect { + "Agree Y/N?" { + send "y\r" + } +} + +expect eof \ No newline at end of file From e78d03ee84e2919d3fbe3e916b67641b3c6fe2c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Goll?= Date: Thu, 15 May 2025 17:24:16 +0200 Subject: [PATCH 13/16] CI: Update actions --- .github/actions/install-slicer/action.yml | 71 ++++++++++++++++--- .../slicer-install-extensions/action.yml | 6 +- .../slicer-install-python-packages/action.yml | 4 +- .../slicer-run-python-script/action.yml | 10 ++- .../slicer-run-python-tests/action.yml | 4 +- 5 files changed, 73 insertions(+), 22 deletions(-) diff --git a/.github/actions/install-slicer/action.yml b/.github/actions/install-slicer/action.yml index b9ebbf7..4515ebd 100644 --- a/.github/actions/install-slicer/action.yml +++ b/.github/actions/install-slicer/action.yml @@ -6,20 +6,73 @@ inputs: description: Version of Slicer available at https://community.chocolatey.org/packages/3dslicer#versionhistory default: '' outputs: - slicer_folder: - description: Installation folder for Slicer - value: ${{ steps.slicer_folder.outputs.slicer_folder }} + slicer_executable: + description: Slicer executable + value: ${{ steps.slicer_exe.outputs.executable }} runs: using: composite steps: - - if: ${{runner.os == 'Windows'}} - run: choco install 3dslicer -y --version=${{ inputs.version }} --allowdowngrade + - name: Setting the permission + run: chmod +x "${{ github.action_path }}/downloader.sh" shell: bash - - if: ${{runner.os == 'Windows'}} - id: slicer_folder + - name: Download installer + run: | + "${{ github.action_path }}/downloader.sh" ${{ runner.os }} + shell: bash + - name: Get installer file + id: installer + run: | + installer=$(find "./installer" -maxdepth 1 -name "Slicer*" | head -n1 | xargs realpath) + echo "slicer_installer=$installer" >> $GITHUB_OUTPUT shell: bash + - name: Windows Slicer install + id: win_install + if: ${{runner.os == 'Windows'}} run: | + ${{ steps.installer.outputs.slicer_installer }} //S folder=$(find "C:/ProgramData/slicer.org" -maxdepth 1 -type d -name Slicer* -exec stat --format "%Y %n" {} + |sort -nr | head -n1 | cut -d' ' -f2-) - echo "slicer_folder=$folder" >> $GITHUB_OUTPUT - + echo "executable=$folder/Slicer.exe" >> $GITHUB_OUTPUT + shell: bash + - name: Linux env setup + if: ${{ runner.os == 'Linux' }} + run: | + sudo apt-get install libglu1-mesa libpulse-mainloop-glib0 libnss3 libasound2t64 qt5dxcb-plugin libxcb-util1 xvfb + mkdir ./Slicer + export DISPLAY=:99 + Xvfb :99 -screen 0 1920x1080x24 > /dev/null 2>&1 & + echo "DISPLAY=:99" >> $GITHUB_ENV + shell: bash + - name: Linux Slicer install + id: linux_install + if: ${{ runner.os == 'Linux' }} + run: | + tar xzf "${{ steps.installer.outputs.slicer_installer }}" -C "." + folder=$(find "." -maxdepth 1 -name "Slicer*" -type d | head -n1 | xargs realpath) + echo "executable=$folder/Slicer" >> $GITHUB_OUTPUT + shell: bash + - name: Max env setup + if: ${{ runner.os == 'macOS' }} + run: | + brew install expect + chmod +x "${{ github.action_path }}/macos_installer.exp" + shell: bash + - name: Mac Slicer install + if: ${{ runner.os == 'macOS' }} + id: mac_install + run: | + MOUNT_POINT=$("${{ github.action_path }}/macos_installer.exp" ${{ steps.installer.outputs.slicer_installer }} | grep -o '/Volumes/[^ ]*'| tr -d '\r\n') + cp -R "$MOUNT_POINT/Slicer.app" /Applications/ + hdiutil detach "$MOUNT_POINT" + echo "executable=/Applications/Slicer.app/Contents/MacOS/Slicer" >> $GITHUB_OUTPUT + shell: bash + - id: slicer_exe + run: | + if [ ${{ runner.os }} == "Windows" ]; then + echo "executable=${{ steps.win_install.outputs.executable }}" >> $GITHUB_OUTPUT + elif [ ${{ runner.os }} == "Linux" ]; then + echo "executable=${{ steps.linux_install.outputs.executable }}" >> $GITHUB_OUTPUT + elif [ ${{ runner.os }} == "macOS" ]; then + echo "executable=${{ steps.mac_install.outputs.executable }}" >> $GITHUB_OUTPUT + fi + shell: bash \ No newline at end of file diff --git a/.github/actions/slicer-install-extensions/action.yml b/.github/actions/slicer-install-extensions/action.yml index ba00a36..aebdc3d 100644 --- a/.github/actions/slicer-install-extensions/action.yml +++ b/.github/actions/slicer-install-extensions/action.yml @@ -3,8 +3,8 @@ author: Sebastien Goll (Kitware SAS) description: Install extensions for Slicer inputs: - slicer_dir: - description: Slicer directory + slicer_exe: + description: Slicer executable required: true additional_arguments: description: Additional arguments for Slicer executable @@ -19,5 +19,5 @@ runs: - uses: ./.github/actions/slicer-run-python-script with: script: ${{ github.action_path }}/install_extensions.py - slicer_dir: ${{ inputs.slicer_dir }} + slicer_exe: ${{ inputs.slicer_exe }} additional_arguments: ${{ inputs.additional_arguments }} ${{ inputs.extensions_name }} diff --git a/.github/actions/slicer-install-python-packages/action.yml b/.github/actions/slicer-install-python-packages/action.yml index 813ece9..d3d1245 100644 --- a/.github/actions/slicer-install-python-packages/action.yml +++ b/.github/actions/slicer-install-python-packages/action.yml @@ -3,7 +3,7 @@ author: Sebastien Goll (Kitware SAS) description: Install python packages in Slicer's python environment inputs: - slicer_dir: + slicer_exe: description: Slicer directory required: true additional_arguments: @@ -18,5 +18,5 @@ runs: - uses: ./.github/actions/slicer-run-python-script with: script: ${{ github.action_path }}/install_py_packages.py - slicer_dir: ${{ inputs.slicer_dir }} + slicer_exe: ${{ inputs.slicer_exe }} additional_arguments: ${{ inputs.additional_arguments }} ${{ inputs.package_names }} diff --git a/.github/actions/slicer-run-python-script/action.yml b/.github/actions/slicer-run-python-script/action.yml index 1b22455..5a26b71 100644 --- a/.github/actions/slicer-run-python-script/action.yml +++ b/.github/actions/slicer-run-python-script/action.yml @@ -2,8 +2,8 @@ name: slicer-run-python-script author: Sebastien Goll (Kitware SAS) description: Run a python script through Slicer inputs: - slicer_dir: - description: Slicer directory + slicer_exe: + description: Slicer executable required: true script: description: Path to python to run @@ -15,7 +15,5 @@ runs: using: composite steps: - run: | - "$SLICER_EXE" --python-script "${{ inputs.script }}" ${{ inputs.additional_arguments }} - shell: bash - env: - SLICER_EXE: ${{inputs.slicer_dir}}/Slicer.exe \ No newline at end of file + "${{inputs.slicer_exe}}" --python-script "${{ inputs.script }}" ${{ inputs.additional_arguments }} + shell: bash \ No newline at end of file diff --git a/.github/actions/slicer-run-python-tests/action.yml b/.github/actions/slicer-run-python-tests/action.yml index a664e3f..bf42d5b 100644 --- a/.github/actions/slicer-run-python-tests/action.yml +++ b/.github/actions/slicer-run-python-tests/action.yml @@ -3,7 +3,7 @@ author: Sebastien Goll (Kitware SAS) description: Run python tests in Slicer inputs: - slicer_dir: + slicer_exe: description: Slicer directory required: true additional_arguments: @@ -25,5 +25,5 @@ runs: - uses: ./.github/actions/slicer-run-python-script with: script: ${{ github.action_path }}/run_tests.py - slicer_dir: ${{ inputs.slicer_dir }} + slicer_exe: ${{ inputs.slicer_exe }} additional_arguments: --additional-module-paths ${{ inputs.module_paths }} ${{ inputs.additional_arguments }} ${{ inputs.tests_root_path }} ${{ inputs.tests_name_pattern }} From dc56c5e3e09ed18e62237d463570aacff054b408 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Goll?= Date: Thu, 15 May 2025 17:24:45 +0200 Subject: [PATCH 14/16] CI: Add matrix strategy to test runner --- .github/workflows/windows-testing.yml | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/windows-testing.yml b/.github/workflows/windows-testing.yml index 4da4ffd..ce4df93 100644 --- a/.github/workflows/windows-testing.yml +++ b/.github/workflows/windows-testing.yml @@ -11,8 +11,13 @@ on: jobs: - Tests-Windows: - runs-on: windows-latest + Running-Tests: + timeout-minutes: 30 + continue-on-error: true + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 name: Checkout @@ -23,18 +28,18 @@ jobs: uses: ./.github/actions/slicer-run-python-script with: script: './.github/python_scripts/install_pytorch_module.py' - slicer_dir: ${{ steps.slicer_install.outputs.slicer_folder }} + slicer_exe: ${{ steps.slicer_install.outputs.slicer_executable }} additional_arguments: --no-main-window --no-splash - name: Install Python dependencies uses: ./.github/actions/slicer-run-python-script with: script: './.github/python_scripts/install_python_dependencies.py' - slicer_dir: ${{ steps.slicer_install.outputs.slicer_folder }} + slicer_exe: ${{ steps.slicer_install.outputs.slicer_executable }} additional_arguments: --no-main-window --no-splash - name: Run tests uses: ./.github/actions/slicer-run-python-tests with: - slicer_dir: ${{ steps.slicer_install.outputs.slicer_folder }} + slicer_exe: ${{ steps.slicer_install.outputs.slicer_executable }} additional_arguments: --testing --no-splash tests_root_path: './MultiverSeg/Testing/Python' tests_name_pattern: '*TestCase.py' From ab7af7b284fdfdbb3e024c27dad946d2bec84aee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Goll?= Date: Thu, 15 May 2025 17:25:13 +0200 Subject: [PATCH 15/16] TEST: Update tests to pass CI on Linux and Mac --- MultiverSeg/Testing/Python/ContextLogicTestCase.py | 12 +++++++++--- .../Testing/Python/SegmentationLogicTestCase.py | 5 +++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/MultiverSeg/Testing/Python/ContextLogicTestCase.py b/MultiverSeg/Testing/Python/ContextLogicTestCase.py index 49fd8ab..b3c5601 100644 --- a/MultiverSeg/Testing/Python/ContextLogicTestCase.py +++ b/MultiverSeg/Testing/Python/ContextLogicTestCase.py @@ -1,5 +1,6 @@ import unittest from pathlib import Path +from shutil import rmtree from unittest import SkipTest import SampleData @@ -15,6 +16,11 @@ def setUpClass(cls): emptyContextPath = Path(__file__).parent.joinpath("../TestData/Context/empty_context").resolve() emptyContextPath.mkdir(exist_ok=True) + @classmethod + def tearDownClass(cls): + emptyContextPath = Path(__file__).parent.joinpath("../TestData/Context/empty_context").resolve() + rmtree(emptyContextPath) + def test_instantiation(self): logic = ContextLogic(None) @@ -102,11 +108,11 @@ def test_saveNewExample(self): iTruth = logic.loadImage(ressourcePath.joinpath("image_0.png")) mTruth = logic.loadImage(ressourcePath.joinpath("mask_0.png")) - diff = torch.abs(i - iTruth) - self.assertEqual(torch.max(diff).item(), 0) + diff = torch.abs(i.to(torch.int16) - iTruth.to(torch.int16)).to(torch.uint8) + self.assertLessEqual(torch.max(diff).item(), 5) # Need tolerance to different compression methods diff = torch.abs(m - mTruth) - self.assertEqual(torch.max(diff).item(), 0) + self.assertLessEqual(torch.max(diff).item(), 5) import shutil shutil.rmtree(contextPath) diff --git a/MultiverSeg/Testing/Python/SegmentationLogicTestCase.py b/MultiverSeg/Testing/Python/SegmentationLogicTestCase.py index c54b358..79576db 100644 --- a/MultiverSeg/Testing/Python/SegmentationLogicTestCase.py +++ b/MultiverSeg/Testing/Python/SegmentationLogicTestCase.py @@ -9,6 +9,11 @@ class SegmentationLogicTestCase(unittest.TestCase): + + @classmethod + def setUpClass(cls): + torch.backends.mps.is_available = lambda : False + def test_instantiation(self): logic = SegmentationLogic(None) From 5e8e785075724c2b0e154592bb340c72f5ae5668 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Goll?= Date: Thu, 15 May 2025 17:25:44 +0200 Subject: [PATCH 16/16] CI: Update script to add current dir to pythonpath --- .github/python_scripts/install_python_dependencies.py | 7 ++++++- .github/python_scripts/install_pytorch_module.py | 8 +++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/python_scripts/install_python_dependencies.py b/.github/python_scripts/install_python_dependencies.py index 436d3e9..2bb121f 100644 --- a/.github/python_scripts/install_python_dependencies.py +++ b/.github/python_scripts/install_python_dependencies.py @@ -1,7 +1,12 @@ +from pathlib import Path + import slicer.util -from MultiverSeg.SegmentEditorMultiverSegLib import InstallLogic, DependenciesLogic +import sys if __name__ == '__main__': + sys.path.append(Path(__file__).parent.joinpath("../..").resolve().as_posix()) + + from MultiverSeg.SegmentEditorMultiverSegLib import InstallLogic, DependenciesLogic DependenciesLogic.INTERACTIVE_MODE = False InstallLogic.INTERACTIVE_MODE = False diff --git a/.github/python_scripts/install_pytorch_module.py b/.github/python_scripts/install_pytorch_module.py index 02da728..63c6263 100644 --- a/.github/python_scripts/install_pytorch_module.py +++ b/.github/python_scripts/install_pytorch_module.py @@ -1,8 +1,14 @@ +import sys +from pathlib import Path + import slicer.util -from MultiverSeg.SegmentEditorMultiverSegLib import DependenciesLogic + if __name__ == '__main__': + sys.path.append(Path(__file__).parent.joinpath("../..").resolve().as_posix()) + + from MultiverSeg.SegmentEditorMultiverSegLib import DependenciesLogic DependenciesLogic.INTERACTIVE_MODE = False DependenciesLogic.installPyTorchExtensionIfNeeded() slicer.util.quit()