diff --git a/.github/workflows/cura-all-installers.yml b/.github/workflows/cura-all-installers.yml
deleted file mode 100644
index 4729c4198a9..00000000000
--- a/.github/workflows/cura-all-installers.yml
+++ /dev/null
@@ -1,151 +0,0 @@
-name: Cura All Installers
-run-name: ${{ inputs.cura_conan_version }} for exe ${{ inputs.build_windows_exe }}, msi ${{ inputs.build_windows_msi }}, dmg ${{ inputs.build_macos }}, pkg ${{ inputs.build_macos_installer }}, appimage ${{ inputs.build_linux }} - enterprise ${{ inputs.enterprise }}
-
-on:
- workflow_dispatch:
- inputs:
- cura_conan_version:
- description: 'Cura Conan Version'
- default: 'cura/latest@ultimaker/testing'
- required: true
- type: string
- conan_args:
- description: 'Conan args: eq.: --require-override'
- default: ''
- required: false
- type: string
- conan_config:
- description: 'Conan config branch to use'
- default: ''
- required: false
- type: string
- enterprise:
- description: 'Build Cura as an Enterprise edition'
- default: false
- required: true
- type: boolean
- staging:
- description: 'Use staging API'
- default: false
- required: true
- type: boolean
- installer:
- description: 'Create the installer'
- default: true
- required: true
- type: boolean
- build_windows_exe:
- description: 'Build for Windows exe'
- default: false
- required: true
- type: boolean
- build_windows_msi:
- description: 'Build for msi+pkg'
- default: true
- required: true
- type: boolean
- build_linux:
- description: 'Build for Linux'
- default: true
- required: true
- type: boolean
- build_macos:
- description: 'Build dmg for MacOS'
- default: true
- required: true
- type: boolean
-
- # Run the nightly at 3:25 UTC on working days
- schedule:
- - cron: '25 3 * * 1-5'
-
-jobs:
- windows-installer-create-exe:
- if: ${{ inputs.build_windows_exe }}
- uses: ./.github/workflows/cura-installer.yml
- with:
- platform: 'windows-2022'
- os_name: 'win64'
- cura_conan_version: ${{ inputs.cura_conan_version }}
- conan_args: ${{ inputs.conan_args }}
- conan_config: ${{ inputs.conan_config }}
- enterprise: ${{ inputs.enterprise }}
- staging: ${{ inputs.staging }}
- installer: ${{ inputs.installer }}
- msi_installer: false
- secrets: inherit
-
- windows-installer-create-msi:
- if: ${{ inputs.build_windows_msi }}
- uses: ./.github/workflows/cura-installer.yml
- with:
- platform: 'windows-2022'
- os_name: 'win64'
- cura_conan_version: ${{ inputs.cura_conan_version }}
- conan_args: ${{ inputs.conan_args }}
- conan_config: ${{ inputs.conan_config }}
- enterprise: ${{ inputs.enterprise }}
- staging: ${{ inputs.staging }}
- installer: ${{ inputs.installer }}
- msi_installer: true
- secrets: inherit
-
- linux-installer-create:
- if: ${{ inputs.build_linux }}
- uses: ./.github/workflows/cura-installer.yml
- with:
- platform: 'ubuntu-20.04'
- os_name: 'linux'
- cura_conan_version: ${{ inputs.cura_conan_version }}
- conan_args: ${{ inputs.conan_args }}
- conan_config: ${{ inputs.conan_config }}
- enterprise: ${{ inputs.enterprise }}
- staging: ${{ inputs.staging }}
- installer: ${{ inputs.installer }}
- msi_installer: false
- secrets: inherit
-
- linux-modern-installer-create:
- if: ${{ inputs.build_linux }}
- uses: ./.github/workflows/cura-installer.yml
- with:
- platform: 'ubuntu-22.04'
- os_name: 'linux-modern'
- cura_conan_version: ${{ inputs.cura_conan_version }}
- conan_args: ${{ inputs.conan_args }}
- conan_config: ${{ inputs.conan_config }}
- enterprise: ${{ inputs.enterprise }}
- staging: ${{ inputs.staging }}
- installer: ${{ inputs.installer }}
- msi_installer: false
- secrets: inherit
-
- macos-dmg-create:
- if: ${{ inputs.build_macos }}
- uses: ./.github/workflows/cura-installer.yml
- with:
- platform: 'macos-11'
- os_name: 'mac'
- cura_conan_version: ${{ inputs.cura_conan_version }}
- conan_args: ${{ inputs.conan_args }}
- conan_config: ${{ inputs.conan_config }}
- enterprise: ${{ inputs.enterprise }}
- staging: ${{ inputs.staging }}
- installer: ${{ inputs.installer }}
- msi_installer: false
- secrets: inherit
-
- macos-installer-create:
- if: ${{ inputs.build_macos }}
- uses: ./.github/workflows/cura-installer.yml
- with:
- platform: 'macos-11'
- os_name: 'mac'
- cura_conan_version: ${{ inputs.cura_conan_version }}
- conan_args: ${{ inputs.conan_args }}
- conan_config: ${{ inputs.conan_config }}
- enterprise: ${{ inputs.enterprise }}
- staging: ${{ inputs.staging }}
- installer: ${{ inputs.installer }}
- msi_installer: true
- secrets: inherit
diff --git a/.github/workflows/cura-installer.yml b/.github/workflows/cura-installer.yml
deleted file mode 100644
index 9572b31aee0..00000000000
--- a/.github/workflows/cura-installer.yml
+++ /dev/null
@@ -1,390 +0,0 @@
-name: Cura Installer
-run-name: ${{ inputs.cura_conan_version }} for ${{ inputs.platform }} by @${{ github.actor }}
-
-on:
- workflow_call:
- inputs:
- platform:
- description: 'Selected Installer OS'
- default: 'ubuntu-20.04'
- required: true
- type: string
- os_name:
- description: 'OS Friendly Name'
- default: 'linux'
- required: true
- type: string
- cura_conan_version:
- description: 'Cura Conan Version'
- default: 'cura/latest@ultimaker/testing'
- required: true
- type: string
- conan_args:
- description: 'Conan args: eq.: --require-override'
- default: ''
- required: false
- type: string
- conan_config:
- description: 'Conan config branch to use'
- default: ''
- required: false
- type: string
- enterprise:
- description: 'Build Cura as an Enterprise edition'
- default: false
- required: true
- type: boolean
- staging:
- description: 'Use staging API'
- default: false
- required: true
- type: boolean
- installer:
- description: 'Create the installer'
- default: true
- required: true
- type: boolean
- msi_installer:
- description: 'Create the msi'
- default: false
- required: true
- type: boolean
-
-env:
- CONAN_LOGIN_USERNAME_CURA: ${{ secrets.CONAN_USER }}
- CONAN_PASSWORD_CURA: ${{ secrets.CONAN_PASS }}
- CONAN_LOGIN_USERNAME_CURA_CE: ${{ secrets.CONAN_USER }}
- CONAN_PASSWORD_CURA_CE: ${{ secrets.CONAN_PASS }}
- CONAN_LOG_RUN_TO_OUTPUT: 1
- CONAN_LOGGING_LEVEL: ${{ inputs.conan_logging_level }}
- CONAN_NON_INTERACTIVE: 1
- CODESIGN_IDENTITY: ${{ secrets.CODESIGN_IDENTITY }}
- MAC_NOTARIZE_USER: ${{ secrets.MAC_NOTARIZE_USER }}
- MAC_NOTARIZE_PASS: ${{ secrets.MAC_NOTARIZE_PASS }}
- MACOS_CERT_P12: ${{ secrets.MACOS_CERT_P12 }}
- MACOS_CERT_INSTALLER_P12: ${{ secrets.MACOS_CERT_INSTALLER_P12 }}
- MACOS_CERT_USER: ${{ secrets.MACOS_CERT_USER }}
- GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
- MACOS_CERT_PASSPHRASE: ${{ secrets.MACOS_CERT_PASSPHRASE }}
- WIN_CERT_INSTALLER_CER: ${{ secrets.WIN_CERT_INSTALLER_CER }}
- WIN_CERT_INSTALLER_CER_PASS: ${{ secrets.WIN_CERT_INSTALLER_CER_PASS }}
- CURA_CONAN_VERSION: ${{ inputs.cura_conan_version }}
- ENTERPRISE: ${{ inputs.enterprise }}
- STAGING: ${{ inputs.staging }}
-
-jobs:
- cura-installer-create:
- runs-on: ${{ inputs.platform }}
-
- steps:
- - name: Checkout
- uses: actions/checkout@v3
-
- - name: Setup Python and pip
- uses: actions/setup-python@v4
- with:
- python-version: '3.10.x'
- cache: 'pip'
- cache-dependency-path: .github/workflows/requirements-conan-package.txt
-
- - name: Install Python requirements for runner
- run: pip install -r https://raw.githubusercontent.com/Ultimaker/Cura/main/.github/workflows/requirements-conan-package.txt
- # Note the runner requirements are always installed from the main branch in the Ultimaker/Cura repo
-
- - name: Use Conan download cache (Bash)
- if: ${{ runner.os != 'Windows' }}
- run: conan config set storage.download_cache="$HOME/.conan/conan_download_cache"
-
- - name: Use Conan download cache (Powershell)
- if: ${{ runner.os == 'Windows' }}
- run: conan config set storage.download_cache="C:\Users\runneradmin\.conan\conan_download_cache"
-
- - name: Cache Conan local repository packages (Bash)
- uses: actions/cache@v3
- if: ${{ runner.os != 'Windows' }}
- with:
- path: |
- $HOME/.conan/data
- $HOME/.conan/conan_download_cache
- key: conan-${{ runner.os }}-${{ runner.arch }}-installer-cache
-
- - name: Cache Conan local repository packages (Powershell)
- uses: actions/cache@v3
- if: ${{ runner.os == 'Windows' }}
- with:
- path: |
- C:\Users\runneradmin\.conan\data
- C:\.conan
- C:\Users\runneradmin\.conan\conan_download_cache
- key: conan-${{ runner.os }}-${{ runner.arch }}-installer-cache
-
- - name: Install MacOS system requirements
- if: ${{ runner.os == 'Macos' }}
- run: brew install autoconf automake ninja create-dmg # Delete create-dmg when deprecating dmg
-
- - name: Hack needed specifically for ubuntu-22.04 from mid-Feb 2023 onwards
- if: ${{ runner.os == 'Linux' && startsWith(inputs.platform, 'ubuntu-22.04') }}
- run: sudo apt remove libodbc2 libodbcinst2 unixodbc-common -y
-
- # NOTE: Due to what are probably github issues, we have to remove the cache and reconfigure before the rest.
- # This is maybe because grub caches the disk it uses last time, which is recreated each time.
- - name: Install Linux system requirements
- if: ${{ runner.os == 'Linux' }}
- run: |
- sudo rm /var/cache/debconf/config.dat
- sudo dpkg --configure -a
- sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y
- sudo apt update
- sudo apt upgrade
- sudo apt install build-essential checkinstall libegl-dev zlib1g-dev libssl-dev ninja-build autoconf libx11-dev libx11-xcb-dev libfontenc-dev libice-dev libsm-dev libxau-dev libxaw7-dev libxcomposite-dev libxcursor-dev libxdamage-dev libxdmcp-dev libxext-dev libxfixes-dev libxi-dev libxinerama-dev libxkbfile-dev libxmu-dev libxmuu-dev libxpm-dev libxrandr-dev libxrender-dev libxres-dev libxss-dev libxt-dev libxtst-dev libxv-dev libxvmc-dev libxxf86vm-dev xtrans-dev libxcb-render0-dev libxcb-render-util0-dev libxcb-xkb-dev libxcb-icccm4-dev libxcb-image0-dev libxcb-keysyms1-dev libxcb-randr0-dev libxcb-shape0-dev libxcb-sync-dev libxcb-xfixes0-dev libxcb-xinerama0-dev xkb-data libxcb-dri3-dev uuid-dev libxcb-util-dev libxkbcommon-x11-dev pkg-config -y
- wget --no-check-certificate --quiet https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage -O $GITHUB_WORKSPACE/appimagetool
- chmod +x $GITHUB_WORKSPACE/appimagetool
- echo "APPIMAGETOOL_LOCATION=$GITHUB_WORKSPACE/appimagetool" >> $GITHUB_ENV
-
- - name: Install GCC-12 on ubuntu-22.04
- if: ${{ startsWith(inputs.platform, 'ubuntu-22.04') }}
- run: |
- sudo apt install g++-12 gcc-12 -y
- sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-12 12
- sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-12 12
-
- - name: Use GCC-10 on ubuntu-20.04
- if: ${{ startsWith(inputs.platform, 'ubuntu-20.04') }}
- run: |
- sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-10 10
- sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-10 10
-
- - name: Create the default Conan profile
- run: conan profile new default --detect
-
- - name: Configure GPG Key Linux (Bash)
- if: ${{ runner.os == 'Linux' }}
- run: echo -n "$GPG_PRIVATE_KEY" | base64 --decode | gpg --import
-
- - name: Configure Macos keychain Developer Cert(Bash)
- id: macos-keychain-developer-cert
- if: ${{ runner.os == 'Macos' }}
- uses: apple-actions/import-codesign-certs@v1
- with:
- keychain-password: ${{ secrets.MACOS_KEYCHAIN_PASSWORD }}
- p12-file-base64: ${{ secrets.MACOS_CERT_P12 }}
- p12-password: ${{ secrets.MACOS_CERT_PASSPHRASE }}
-
- - name: Configure Macos keychain Installer Cert (Bash)
- id: macos-keychain-installer-cert
- if: ${{ runner.os == 'Macos' }}
- uses: apple-actions/import-codesign-certs@v1
- with:
- keychain-password: ${{ secrets.MACOS_KEYCHAIN_PASSWORD }}
- create-keychain: false # keychain is created in previous use of action.
- p12-file-base64: ${{ secrets.MACOS_CERT_INSTALLER_P12 }}
- p12-password: ${{ secrets.MACOS_CERT_PASSPHRASE }}
-
- - name: Create PFX certificate from BASE64_PFX_CONTENT secret
- if: ${{ runner.os == 'Windows' }}
- id: create-pfx
- env:
- PFX_CONTENT: ${{ secrets.WIN_CERT_INSTALLER_CER }}
- run: |
- $pfxPath = Join-Path -Path $env:RUNNER_TEMP -ChildPath "cert.pfx";
- $encodedBytes = [System.Convert]::FromBase64String($env:PFX_CONTENT);
- Set-Content $pfxPath -Value $encodedBytes -AsByteStream;
- echo "PFX_PATH=$pfxPath" >> $env:GITHUB_OUTPUT;
-
- - name: Get Conan configuration from branch
- if: ${{ inputs.conan_config != '' }}
- run: conan config install https://github.com/Ultimaker/conan-config.git -a "-b ${{ inputs.conan_config }}"
-
- - name: Get Conan configuration
- if: ${{ inputs.conan_config == '' }}
- run: conan config install https://github.com/Ultimaker/conan-config.git
-
- - name: Create the Packages (Bash)
- if: ${{ runner.os != 'Windows' }}
- run: conan install $CURA_CONAN_VERSION ${{ inputs.conan_args }} --build=missing --update -if cura_inst -g VirtualPythonEnv -o cura:enterprise=$ENTERPRISE -o cura:staging=$STAGING --json "cura_inst/conan_install_info.json"
-
- - name: Create the Packages (Powershell)
- if: ${{ runner.os == 'Windows' }}
- run: conan install $Env:CURA_CONAN_VERSION ${{ inputs.conan_args }} --build=missing --update -if cura_inst -g VirtualPythonEnv -o cura:enterprise=$Env:ENTERPRISE -o cura:staging=$Env:STAGING --json "cura_inst/conan_install_info.json"
-
- - name: Set Environment variables for Cura (bash)
- if: ${{ runner.os != 'Windows' }}
- run: |
- . ./cura_inst/bin/activate_github_actions_env.sh
- . ./cura_inst/bin/activate_github_actions_version_env.sh
-
- - name: Set Environment variables for Cura (Powershell)
- if: ${{ runner.os == 'Windows' }}
- run: |
- echo "${Env:WIX}\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- .\cura_inst\Scripts\activate_github_actions_env.ps1
- .\cura_inst\Scripts\activate_github_actions_version_env.ps1
-
- - name: Unlock Macos keychain (Bash)
- if: ${{ runner.os == 'Macos' }}
- run: security unlock -p $TEMP_KEYCHAIN_PASSWORD signing_temp.keychain
- env:
- TEMP_KEYCHAIN_PASSWORD: ${{ steps.macos-keychain-developer-cert.outputs.keychain-password }}
-
- # FIXME: This is a workaround to ensure that we use and pack a shared library for OpenSSL 1.1.1l. We currently compile
- # OpenSSL statically for CPython, but our Python Dependenies (such as PyQt6) require a shared library.
- # Because Conan won't allow for building the same library with two different options (easily) we need to install it explicitly
- # and do a manual copy to the VirtualEnv, such that Pyinstaller can find it.
-
- - name: Install OpenSSL shared
- run: conan install openssl/1.1.1l@_/_ --build=missing --update -o openssl:shared=True -g deploy
-
- - name: Copy OpenSSL shared (Bash)
- if: ${{ runner.os != 'Windows' }}
- run: |
- cp ./openssl/lib/*.so* ./cura_inst/bin/ || true
- cp ./openssl/lib/*.dylib* ./cura_inst/bin/ || true
-
- - name: Copy OpenSSL shared (Powershell)
- if: ${{ runner.os == 'Windows' }}
- run: |
- cp openssl/bin/*.dll ./cura_inst/Scripts/
- cp openssl/lib/*.lib ./cura_inst/Lib/
-
- - name: Create the Cura dist
- run: pyinstaller ./cura_inst/UltiMaker-Cura.spec
-
- - name: Output the name file name and extension
- id: filename
- shell: python
- run: |
- import os
- enterprise = "-Enterprise" if "${{ inputs.enterprise }}" == "true" else ""
- installer_filename = f"UltiMaker-Cura-{os.getenv('CURA_VERSION_FULL')}{enterprise}-${{ inputs.os_name }}"
- if "${{ runner.os }}" == "Windows":
- installer_ext = "msi" if "${{ inputs.msi_installer }}" == "true" else "exe"
- elif "${{ runner.os }}" == "macOS":
- installer_ext = "pkg" if "${{ inputs.msi_installer }}" == "true" else "dmg"
- else:
- installer_ext = "AppImage"
- output_env = os.environ["GITHUB_OUTPUT"]
- content = ""
- if os.path.exists(output_env):
- with open(output_env, "r") as f:
- content = f.read()
- with open(output_env, "w") as f:
- f.write(content)
- f.writelines(f"INSTALLER_FILENAME={installer_filename}\n")
- f.writelines(f"INSTALLER_EXT={installer_ext}\n")
- f.writelines(f"FULL_INSTALLER_FILENAME={installer_filename}.{installer_ext}\n")
-
- - name: Summarize the used Conan dependencies
- shell: python
- run: |
- import os
- import json
- from pathlib import Path
-
- conan_install_info_path = Path("cura_inst/conan_install_info.json")
- conan_info = {"installed": []}
- if os.path.exists(conan_install_info_path):
- with open(conan_install_info_path, "r") as f:
- conan_info = json.load(f)
- sorted_deps = sorted([dep["recipe"]["id"].replace('#', r' rev: ') for dep in conan_info["installed"]])
-
- summary_env = os.environ["GITHUB_STEP_SUMMARY"]
- content = ""
- if os.path.exists(summary_env):
- with open(summary_env, "r") as f:
- content = f.read()
-
- with open(summary_env, "w") as f:
- f.write(content)
- f.writelines("# ${{ steps.filename.outputs.FULL_INSTALLER_FILENAME }}\n")
- f.writelines("## Conan packages:\n")
- for dep in sorted_deps:
- f.writelines(f"`{dep}`\n")
-
- - name: Summarize the used Python modules
- shell: python
- run: |
- import os
- import pkg_resources
- summary_env = os.environ["GITHUB_STEP_SUMMARY"]
- content = ""
- if os.path.exists(summary_env):
- with open(summary_env, "r") as f:
- content = f.read()
-
- with open(summary_env, "w") as f:
- f.write(content)
- f.writelines("## Python modules:\n")
- for package in pkg_resources.working_set:
- f.writelines(f"`{package.key}/{package.version}`\n")
-
- - name: Archive the artifacts (bash)
- if: ${{ !inputs.installer && runner.os != 'Windows' }}
- run: tar -zcf "./${{ steps.filename.outputs.INSTALLER_FILENAME }}.tar.gz" "./UltiMaker-Cura/"
- working-directory: dist
-
- - name: Archive the artifacts (Powershell)
- if: ${{ !inputs.installer && runner.os == 'Windows' }}
- run: Compress-Archive -Path ".\UltiMaker-Cura" -DestinationPath ".\${{ steps.filename.outputs.INSTALLER_FILENAME }}.zip"
- working-directory: dist
-
- - name: Create the Windows exe installer (Powershell)
- if: ${{ inputs.installer && runner.os == 'Windows' && !inputs.msi_installer }}
- run: |
- python ..\cura_inst\packaging\NSIS\create_windows_installer.py ../cura_inst . "${{ steps.filename.outputs.FULL_INSTALLER_FILENAME }}"
- working-directory: dist
-
- - name: Create the Windows msi installer (Powershell)
- if: ${{ inputs.installer && runner.os == 'Windows' && inputs.msi_installer }}
- run: |
- python ..\cura_inst\packaging\msi\create_windows_msi.py ..\cura_inst .\UltiMaker-Cura "${{ steps.filename.outputs.FULL_INSTALLER_FILENAME }}" "$Env:CURA_APP_NAME"
- working-directory: dist
-
- - name: Sign the Windows exe installer (Powershell)
- if: ${{ inputs.installer && runner.os == 'Windows' && !inputs.msi_installer }}
- env:
- PFX_PATH: ${{ steps.create-pfx.outputs.PFX_PATH }}
- run: |
- & "C:/Program Files (x86)/Windows Kits/10/bin/10.0.17763.0/x86/signtool.exe" sign /f $Env:PFX_PATH /p "$Env:WIN_CERT_INSTALLER_CER_PASS" /fd SHA256 /t http://timestamp.digicert.com "${{ steps.filename.outputs.FULL_INSTALLER_FILENAME }}"
- working-directory: dist
-
- - name: Sign the Windows msi installer (Powershell)
- if: ${{ inputs.installer && runner.os == 'Windows' && inputs.msi_installer }}
- env:
- PFX_PATH: ${{ steps.create-pfx.outputs.PFX_PATH }}
- run: |
- & "C:/Program Files (x86)/Windows Kits/10/bin/10.0.17763.0/x86/signtool.exe" sign /f $Env:PFX_PATH /p "$Env:WIN_CERT_INSTALLER_CER_PASS" /fd SHA256 /t http://timestamp.digicert.com "${{ steps.filename.outputs.FULL_INSTALLER_FILENAME }}"
- working-directory: dist
-
- - name: Create the Linux AppImage (Bash)
- if: ${{ inputs.installer && runner.os == 'Linux' }}
- run: python ../cura_inst/packaging/AppImage/create_appimage.py ./UltiMaker-Cura $CURA_VERSION_FULL "${{ steps.filename.outputs.FULL_INSTALLER_FILENAME }}"
- working-directory: dist
-
- - name: Create the MacOS dmg and/or pkg (Bash)
- if: ${{ github.event.inputs.installer == 'true' && runner.os == 'Macos' }}
- run: python ../cura_inst/packaging/MacOS/build_macos.py ../cura_inst . $CURA_CONAN_VERSION "${{ steps.filename.outputs.FULL_INSTALLER_FILENAME }}" "$CURA_APP_NAME"
- working-directory: dist
-
- - name: Upload the artifacts
- uses: actions/upload-artifact@v3
- with:
- name: ${{ steps.filename.outputs.INSTALLER_FILENAME }}-${{ steps.filename.outputs.INSTALLER_EXT }}
- path: |
- dist/*.tar.gz
- dist/*.zip
- dist/${{ steps.filename.outputs.FULL_INSTALLER_FILENAME }}
- dist/*.asc
- retention-days: 5
-
- notify-export:
- if: ${{ always() }}
- needs: [ cura-installer-create ]
-
- uses: ultimaker/cura/.github/workflows/notify.yml@main
- with:
- success: ${{ contains(join(needs.*.result, ','), 'success') }}
- success_title: "Create the Cura distributions"
- success_body: "Installers for ${{ inputs.cura_conan_version }}"
- failure_title: "Failed to create the Cura distributions"
- failure_body: "Failed to create at least 1 installer for ${{ inputs.cura_conan_version }}"
- secrets: inherit
diff --git a/.github/workflows/installers.yml b/.github/workflows/installers.yml
new file mode 100644
index 00000000000..ead3b7a87a4
--- /dev/null
+++ b/.github/workflows/installers.yml
@@ -0,0 +1,82 @@
+name: All installers
+run-name: ${{ inputs.cura_conan_version }} by @${{ github.actor }}
+
+on:
+ workflow_dispatch:
+ inputs:
+ cura_conan_version:
+ description: 'Cura Conan Version'
+ default: 'cura/latest@ultimaker/testing'
+ required: true
+ type: string
+ conan_args:
+ description: 'Conan args: eq.: --require-override'
+ default: ''
+ required: false
+ type: string
+ enterprise:
+ description: 'Build Cura as an Enterprise edition'
+ default: false
+ required: true
+ type: boolean
+ staging:
+ description: 'Use staging API'
+ default: false
+ required: true
+ type: boolean
+
+jobs:
+ windows-installer:
+ uses: ./.github/workflows/windows.yml
+ with:
+ cura_conan_version: ${{ inputs.cura_conan_version }}
+ conan_args: ${{ inputs.conan_args }}
+ enterprise: ${{ inputs.enterprise }}
+ staging: ${{ inputs.staging }}
+ architecture: X64
+ operating_system: windows-2022
+ secrets: inherit
+
+ linux-modern-installer:
+ uses: ./.github/workflows/linux.yml
+ with:
+ cura_conan_version: ${{ inputs.cura_conan_version }}
+ conan_args: ${{ inputs.conan_args }}
+ enterprise: ${{ inputs.enterprise }}
+ staging: ${{ inputs.staging }}
+ architecture: X64
+ operating_system: ubuntu-22.04
+ secrets: inherit
+
+ linux-legacy-installer:
+ uses: ./.github/workflows/linux.yml
+ with:
+ cura_conan_version: ${{ inputs.cura_conan_version }}
+ conan_args: ${{ inputs.conan_args }}
+ enterprise: ${{ inputs.enterprise }}
+ staging: ${{ inputs.staging }}
+ architecture: X64
+ operating_system: ubuntu-20.04
+ secrets: inherit
+
+ macos-installer:
+ uses: ./.github/workflows/macos.yml
+ with:
+ cura_conan_version: ${{ inputs.cura_conan_version }}
+ conan_args: ${{ inputs.conan_args }}
+ enterprise: ${{ inputs.enterprise }}
+ staging: ${{ inputs.staging }}
+ architecture: X64
+ operating_system: macos-11.0
+ secrets: inherit
+
+ macos-arm-installer:
+ uses: ./.github/workflows/macos.yml
+ with:
+ cura_conan_version: ${{ inputs.cura_conan_version }}
+ conan_args: ${{ inputs.conan_args }}
+ enterprise: ${{ inputs.enterprise }}
+ staging: ${{ inputs.staging }}
+ architecture: ARM64
+ operating_system: self-hosted
+ secrets: inherit
\ No newline at end of file
diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml
new file mode 100644
index 00000000000..5a18199e3cb
--- /dev/null
+++ b/.github/workflows/linux.yml
@@ -0,0 +1,271 @@
+name: Linux Installer
+run-name: ${{ inputs.cura_conan_version }} for Linux-${{ inputs.architecture }} by @${{ github.actor }}
+
+on:
+ workflow_dispatch:
+ inputs:
+ cura_conan_version:
+ description: 'Cura Conan Version'
+ default: 'cura/latest@ultimaker/testing'
+ required: true
+ type: string
+ conan_args:
+ description: 'Conan args: eq.: --require-override'
+ default: ''
+ required: false
+ type: string
+ enterprise:
+ description: 'Build Cura as an Enterprise edition'
+ default: false
+ required: true
+ type: boolean
+ staging:
+ description: 'Use staging API'
+ default: false
+ required: true
+ type: boolean
+ architecture:
+ description: 'Architecture'
+ required: true
+ default: 'X64'
+ type: choice
+ options:
+ - X64
+ operating_system:
+ description: 'OS'
+ required: true
+ default: 'ubuntu-22.04'
+ type: choice
+ options:
+ - ubuntu-22.04
+ - ubuntu-20.04
+ workflow_call:
+ inputs:
+ cura_conan_version:
+ description: 'Cura Conan Version'
+ default: 'cura/latest@ultimaker/testing'
+ required: true
+ type: string
+ conan_args:
+ description: 'Conan args: eq.: --require-override'
+ default: ''
+ required: false
+ type: string
+ enterprise:
+ description: 'Build Cura as an Enterprise edition'
+ default: false
+ required: true
+ type: boolean
+ staging:
+ description: 'Use staging API'
+ default: false
+ required: true
+ type: boolean
+ architecture:
+ description: 'Architecture'
+ required: true
+ default: 'X64'
+ type: string
+ operating_system:
+ description: 'OS'
+ required: true
+ default: 'ubuntu-22.04'
+ type: string
+
+env:
+ CONAN_LOGIN_USERNAME_CURA: ${{ secrets.CONAN_USER }}
+ CONAN_PASSWORD_CURA: ${{ secrets.CONAN_PASS }}
+ GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
+ CURA_CONAN_VERSION: ${{ inputs.cura_conan_version }}
+ ENTERPRISE: ${{ inputs.enterprise }}
+ STAGING: ${{ inputs.staging }}
+
+jobs:
+ cura-installer-create:
+ runs-on: ${{ inputs.operating_system }}
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: Setup Python and pip
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.10.x'
+ cache: 'pip'
+ cache-dependency-path: .github/workflows/requirements-conan-package.txt
+
+ - name: Install Python requirements for runner
+ run: pip install -r .github/workflows/requirements-conan-package.txt
+
+ - name: Cache Conan local repository packages (Bash)
+ uses: actions/cache@v3
+ with:
+ path: |
+ $HOME/.conan/data
+ $HOME/.conan/conan_download_cache
+ key: conan-${{ runner.os }}-${{ runner.arch }}-installer-cache
+
+ - name: Hack needed specifically for ubuntu-22.04 from mid-Feb 2023 onwards
+ if: ${{ startsWith(inputs.operating_system, 'ubuntu-22.04') }}
+ run: sudo apt remove libodbc2 libodbcinst2 unixodbc-common -y
+
+ # NOTE: Due to what are probably github issues, we have to remove the cache and reconfigure before the rest.
+ # This is maybe because grub caches the disk it uses last time, which is recreated each time.
+ - name: Install Linux system requirements
+ run: |
+ sudo rm /var/cache/debconf/config.dat
+ sudo dpkg --configure -a
+ sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y
+ sudo apt update
+ sudo apt upgrade
+ sudo apt install build-essential checkinstall libegl-dev zlib1g-dev libssl-dev ninja-build autoconf libx11-dev libx11-xcb-dev libfontenc-dev libice-dev libsm-dev libxau-dev libxaw7-dev libxcomposite-dev libxcursor-dev libxdamage-dev libxdmcp-dev libxext-dev libxfixes-dev libxi-dev libxinerama-dev libxkbfile-dev libxmu-dev libxmuu-dev libxpm-dev libxrandr-dev libxrender-dev libxres-dev libxss-dev libxt-dev libxtst-dev libxv-dev libxvmc-dev libxxf86vm-dev xtrans-dev libxcb-render0-dev libxcb-render-util0-dev libxcb-xkb-dev libxcb-icccm4-dev libxcb-image0-dev libxcb-keysyms1-dev libxcb-randr0-dev libxcb-shape0-dev libxcb-sync-dev libxcb-xfixes0-dev libxcb-xinerama0-dev xkb-data libxcb-dri3-dev uuid-dev libxcb-util-dev libxkbcommon-x11-dev pkg-config -y
+ wget --no-check-certificate --quiet https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage -O $GITHUB_WORKSPACE/appimagetool
+ chmod +x $GITHUB_WORKSPACE/appimagetool
+ echo "APPIMAGETOOL_LOCATION=$GITHUB_WORKSPACE/appimagetool" >> $GITHUB_ENV
+
+ - name: Install GCC-12 on ubuntu-22.04
+ if: ${{ startsWith(inputs.operating_system, 'ubuntu-22.04') }}
+ run: |
+ sudo apt install g++-12 gcc-12 -y
+ sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-12 12
+ sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-12 12
+
+ - name: Use GCC-10 on ubuntu-20.04
+ if: ${{ startsWith(inputs.operating_system, 'ubuntu-20.04') }}
+ run: |
+ sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-10 10
+ sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-10 10
+
+ - name: Create the default Conan profile
+ run: conan profile new default --detect --force
+
+ - name: Configure GPG Key Linux (Bash)
+ run: echo -n "$GPG_PRIVATE_KEY" | base64 --decode | gpg --import
+
+ - name: Get Conan configuration
+ run: |
+ conan config install https://github.com/Ultimaker/conan-config.git
+ conan config install https://github.com/Ultimaker/conan-config.git -a "-b runner/${{ runner.os }}/${{ runner.arch }}"
+
+ - name: Use Conan download cache (Bash)
+ run: conan config set storage.download_cache="$HOME/.conan/conan_download_cache"
+
+ - name: Create the Packages (Bash)
+ run: conan install $CURA_CONAN_VERSION ${{ inputs.conan_args }} --build=missing --update -if cura_inst -g VirtualPythonEnv -o cura:enterprise=$ENTERPRISE -o cura:staging=$STAGING --json "cura_inst/conan_install_info.json"
+
+ - name: Upload the Package(s)
+ if: always()
+ run: |
+ conan upload "*" -r cura --all -c
+
+ - name: Set Environment variables for Cura (bash)
+ run: |
+ . ./cura_inst/bin/activate_github_actions_env.sh
+ . ./cura_inst/bin/activate_github_actions_version_env.sh
+
+ # FIXME: This is a workaround to ensure that we use and pack a shared library for OpenSSL 1.1.1l. We currently compile
+ # OpenSSL statically for CPython, but our Python Dependenies (such as PyQt6) require a shared library.
+ # Because Conan won't allow for building the same library with two different options (easily) we need to install it explicitly
+ # and do a manual copy to the VirtualEnv, such that Pyinstaller can find it.
+
+ - name: Install OpenSSL shared
+ run: conan install openssl/1.1.1l@_/_ --build=missing --update -o openssl:shared=True -g deploy
+
+ - name: Copy OpenSSL shared (Bash)
+ run: |
+ cp ./openssl/lib/*.so* ./cura_inst/bin/ || true
+ cp ./openssl/lib/*.dylib* ./cura_inst/bin/ || true
+
+ - name: Create the Cura dist
+ run: pyinstaller ./cura_inst/UltiMaker-Cura.spec
+
+ - name: Output the name file name and extension
+ id: filename
+ shell: python
+ run: |
+ import os
+ enterprise = "-Enterprise" if "${{ inputs.enterprise }}" == "true" else ""
+ if "${{ inputs.operating_system }}" == "ubuntu-22.04":
+ installer_filename = f"UltiMaker-Cura-{os.getenv('CURA_VERSION_FULL')}{enterprise}-linux-modern-${{ inputs.architecture }}"
+ else:
+ installer_filename = f"UltiMaker-Cura-{os.getenv('CURA_VERSION_FULL')}{enterprise}-linux-${{ inputs.architecture }}"
+ output_env = os.environ["GITHUB_OUTPUT"]
+ content = ""
+ if os.path.exists(output_env):
+ with open(output_env, "r") as f:
+ content = f.read()
+ with open(output_env, "w") as f:
+ f.write(content)
+ f.writelines(f"INSTALLER_FILENAME={installer_filename}\n")
+
+ - name: Summarize the used Conan dependencies
+ shell: python
+ run: |
+ import os
+ import json
+ from pathlib import Path
+
+ conan_install_info_path = Path("cura_inst/conan_install_info.json")
+ conan_info = {"installed": []}
+ if os.path.exists(conan_install_info_path):
+ with open(conan_install_info_path, "r") as f:
+ conan_info = json.load(f)
+ sorted_deps = sorted([dep["recipe"]["id"].replace('#', r' rev: ') for dep in conan_info["installed"]])
+
+ summary_env = os.environ["GITHUB_STEP_SUMMARY"]
+ content = ""
+ if os.path.exists(summary_env):
+ with open(summary_env, "r") as f:
+ content = f.read()
+
+ with open(summary_env, "w") as f:
+ f.write(content)
+ f.writelines("# ${{ steps.filename.outputs.INSTALLER_FILENAME }}\n")
+ f.writelines("## Conan packages:\n")
+ for dep in sorted_deps:
+ f.writelines(f"`{dep}`\n")
+
+ - name: Summarize the used Python modules
+ shell: python
+ run: |
+ import os
+ import pkg_resources
+ summary_env = os.environ["GITHUB_STEP_SUMMARY"]
+ content = ""
+ if os.path.exists(summary_env):
+ with open(summary_env, "r") as f:
+ content = f.read()
+
+ with open(summary_env, "w") as f:
+ f.write(content)
+ f.writelines("## Python modules:\n")
+ for package in pkg_resources.working_set:
+ f.writelines(f"`{package.key}/{package.version}`\n")
+
+ - name: Create the Linux AppImage (Bash)
+ run: |
+ python ../cura_inst/packaging/AppImage/create_appimage.py ./UltiMaker-Cura $CURA_VERSION_FULL "${{ steps.filename.outputs.INSTALLER_FILENAME }}.AppImage"
+ chmod +x "${{ steps.filename.outputs.INSTALLER_FILENAME }}.AppImage"
+ working-directory: dist
+
+ - name: Upload the AppImage
+ uses: actions/upload-artifact@v3
+ with:
+ name: ${{ steps.filename.outputs.INSTALLER_FILENAME }}-AppImage
+ path: |
+ dist/${{ steps.filename.outputs.INSTALLER_FILENAME }}.AppImage
+ retention-days: 5
+
+ notify-export:
+ if: ${{ always() }}
+ needs: [ cura-installer-create ]
+
+ uses: ultimaker/cura/.github/workflows/notify.yml@main
+ with:
+ success: ${{ contains(join(needs.*.result, ','), 'success') }}
+ success_title: "Create the Cura distributions"
+ success_body: "Installers for ${{ inputs.cura_conan_version }}"
+ failure_title: "Failed to create the Cura distributions"
+ failure_body: "Failed to create at least 1 installer for ${{ inputs.cura_conan_version }}"
+ secrets: inherit
diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml
new file mode 100644
index 00000000000..af1fc3d12be
--- /dev/null
+++ b/.github/workflows/macos.yml
@@ -0,0 +1,279 @@
+name: Macos Installer
+run-name: ${{ inputs.cura_conan_version }} for Macos-${{ inputs.architecture }} by @${{ github.actor }}
+
+on:
+ workflow_dispatch:
+ inputs:
+ cura_conan_version:
+ description: 'Cura Conan Version'
+ default: 'cura/latest@ultimaker/testing'
+ required: true
+ type: string
+ conan_args:
+ description: 'Conan args: eq.: --require-override'
+ default: ''
+ required: false
+ type: string
+ enterprise:
+ description: 'Build Cura as an Enterprise edition'
+ default: false
+ required: true
+ type: boolean
+ staging:
+ description: 'Use staging API'
+ default: false
+ required: true
+ type: boolean
+ architecture:
+ description: 'Architecture'
+ required: true
+ default: 'X64'
+ type: choice
+ options:
+ - X64
+ - ARM64
+ operating_system:
+ description: 'OS'
+ required: true
+ default: 'macos-11'
+ type: choice
+ options:
+ - self-hosted
+ - macos-11
+ - macos-12
+ workflow_call:
+ inputs:
+ cura_conan_version:
+ description: 'Cura Conan Version'
+ default: 'cura/latest@ultimaker/testing'
+ required: true
+ type: string
+ conan_args:
+ description: 'Conan args: eq.: --require-override'
+ default: ''
+ required: false
+ type: string
+ enterprise:
+ description: 'Build Cura as an Enterprise edition'
+ default: false
+ required: true
+ type: boolean
+ staging:
+ description: 'Use staging API'
+ default: false
+ required: true
+ type: boolean
+ architecture:
+ description: 'Architecture'
+ required: true
+ default: 'X64'
+ type: string
+ operating_system:
+ description: 'OS'
+ required: true
+ default: 'macos-11'
+ type: string
+
+env:
+ CONAN_LOGIN_USERNAME_CURA: ${{ secrets.CONAN_USER }}
+ CONAN_PASSWORD_CURA: ${{ secrets.CONAN_PASS }}
+ CODESIGN_IDENTITY: ${{ secrets.CODESIGN_IDENTITY }}
+ MAC_NOTARIZE_USER: ${{ secrets.MAC_NOTARIZE_USER }}
+ MAC_NOTARIZE_PASS: ${{ secrets.MAC_NOTARIZE_PASS }}
+ MACOS_CERT_P12: ${{ secrets.MACOS_CERT_P12 }}
+ MACOS_CERT_INSTALLER_P12: ${{ secrets.MACOS_CERT_INSTALLER_P12 }}
+ MACOS_CERT_USER: ${{ secrets.MACOS_CERT_USER }}
+ MACOS_CERT_PASSPHRASE: ${{ secrets.MACOS_CERT_PASSPHRASE }}
+ CURA_CONAN_VERSION: ${{ inputs.cura_conan_version }}
+ ENTERPRISE: ${{ inputs.enterprise }}
+ STAGING: ${{ inputs.staging }}
+
+jobs:
+ cura-installer-create:
+ runs-on: ${{ inputs.operating_system }}
+
+ outputs:
+ INSTALLER_FILENAME: ${{ steps.filename.outputs.INSTALLER_FILENAME }}
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: Setup Python and pip
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.10.x'
+ cache: 'pip'
+ cache-dependency-path: .github/workflows/requirements-conan-package.txt
+
+ - name: Install Python requirements for runner
+ run: pip install -r .github/workflows/requirements-conan-package.txt
+
+ - name: Cache Conan local repository packages (Bash)
+ uses: actions/cache@v3
+ with:
+ path: |
+ $HOME/.conan/data
+ $HOME/.conan/conan_download_cache
+ key: conan-${{ runner.os }}-${{ runner.arch }}-installer-cache
+
+ - name: Install MacOS system requirements
+ run: brew install cmake autoconf automake ninja create-dmg
+
+ - name: Create the default Conan profile
+ run: conan profile new default --detect --force
+
+ - name: Remove Macos keychain (Bash)
+ run: security delete-keychain signing_temp.keychain || true
+
+ - name: Configure Macos keychain Developer Cert(Bash)
+ id: macos-keychain-developer-cert
+ uses: apple-actions/import-codesign-certs@v1
+ with:
+ keychain-password: ${{ secrets.MACOS_KEYCHAIN_PASSWORD }}
+ p12-file-base64: ${{ secrets.MACOS_CERT_P12 }}
+ p12-password: ${{ secrets.MACOS_CERT_PASSPHRASE }}
+
+ - name: Configure Macos keychain Installer Cert (Bash)
+ id: macos-keychain-installer-cert
+ uses: apple-actions/import-codesign-certs@v1
+ with:
+ keychain-password: ${{ secrets.MACOS_KEYCHAIN_PASSWORD }}
+ create-keychain: false # keychain is created in previous use of action.
+ p12-file-base64: ${{ secrets.MACOS_CERT_INSTALLER_P12 }}
+ p12-password: ${{ secrets.MACOS_CERT_PASSPHRASE }}
+
+ - name: Get Conan configuration
+ run: |
+ conan config install https://github.com/Ultimaker/conan-config.git
+ conan config install https://github.com/Ultimaker/conan-config.git -a "-b runner/${{ runner.os }}/${{ runner.arch }}"
+
+ - name: Use Conan download cache (Bash)
+ run: conan config set storage.download_cache="$HOME/.conan/conan_download_cache"
+
+ - name: Create the Packages (Bash)
+ run: conan install $CURA_CONAN_VERSION ${{ inputs.conan_args }} --build=missing --update -if cura_inst -g VirtualPythonEnv -o cura:enterprise=$ENTERPRISE -o cura:staging=$STAGING --json "cura_inst/conan_install_info.json"
+
+ - name: Upload the Package(s)
+ if: always()
+ run: |
+ conan upload "*" -r cura --all -c
+
+ - name: Set Environment variables for Cura (bash)
+ run: |
+ . ./cura_inst/bin/activate_github_actions_env.sh
+ . ./cura_inst/bin/activate_github_actions_version_env.sh
+
+ - name: Unlock Macos keychain (Bash)
+ run: security unlock -p $TEMP_KEYCHAIN_PASSWORD signing_temp.keychain
+ env:
+ TEMP_KEYCHAIN_PASSWORD: ${{ steps.macos-keychain-developer-cert.outputs.keychain-password }}
+
+ # FIXME: This is a workaround to ensure that we use and pack a shared library for OpenSSL 1.1.1l. We currently compile
+ # OpenSSL statically for CPython, but our Python Dependenies (such as PyQt6) require a shared library.
+ # Because Conan won't allow for building the same library with two different options (easily) we need to install it explicitly
+ # and do a manual copy to the VirtualEnv, such that Pyinstaller can find it.
+ - name: Install OpenSSL shared
+ run: conan install openssl/1.1.1l@_/_ --build=missing --update -o openssl:shared=True -g deploy
+
+ - name: Copy OpenSSL shared (Bash)
+ run: |
+ cp ./openssl/lib/*.so* ./cura_inst/bin/ || true
+ cp ./openssl/lib/*.dylib* ./cura_inst/bin/ || true
+
+ - name: Create the Cura dist
+ run: pyinstaller ./cura_inst/UltiMaker-Cura.spec
+
+ - name: Output the name file name and extension
+ id: filename
+ shell: python
+ run: |
+ import os
+ enterprise = "-Enterprise" if "${{ inputs.enterprise }}" == "true" else ""
+ installer_filename = f"UltiMaker-Cura-{os.getenv('CURA_VERSION_FULL')}{enterprise}-macos-${{ inputs.architecture }}"
+ output_env = os.environ["GITHUB_OUTPUT"]
+ content = ""
+ if os.path.exists(output_env):
+ with open(output_env, "r") as f:
+ content = f.read()
+ with open(output_env, "w") as f:
+ f.write(content)
+ f.writelines(f"INSTALLER_FILENAME={installer_filename}\n")
+
+ - name: Summarize the used Conan dependencies
+ shell: python
+ run: |
+ import os
+ import json
+ from pathlib import Path
+
+ conan_install_info_path = Path("cura_inst/conan_install_info.json")
+ conan_info = {"installed": []}
+ if os.path.exists(conan_install_info_path):
+ with open(conan_install_info_path, "r") as f:
+ conan_info = json.load(f)
+ sorted_deps = sorted([dep["recipe"]["id"].replace('#', r' rev: ') for dep in conan_info["installed"]])
+
+ summary_env = os.environ["GITHUB_STEP_SUMMARY"]
+ content = ""
+ if os.path.exists(summary_env):
+ with open(summary_env, "r") as f:
+ content = f.read()
+
+ with open(summary_env, "w") as f:
+ f.write(content)
+ f.writelines("# ${{ steps.filename.outputs.INSTALLER_FILENAME }}\n")
+ f.writelines("## Conan packages:\n")
+ for dep in sorted_deps:
+ f.writelines(f"`{dep}`\n")
+
+ - name: Summarize the used Python modules
+ shell: python
+ run: |
+ import os
+ import pkg_resources
+ summary_env = os.environ["GITHUB_STEP_SUMMARY"]
+ content = ""
+ if os.path.exists(summary_env):
+ with open(summary_env, "r") as f:
+ content = f.read()
+
+ with open(summary_env, "w") as f:
+ f.write(content)
+ f.writelines("## Python modules:\n")
+ for package in pkg_resources.working_set:
+ f.writelines(f"`{package.key}/{package.version}`\n")
+
+ - name: Create the Macos dmg (Bash)
+ run: python ../cura_inst/packaging/MacOS/build_macos.py --source_path ../cura_inst --dist_path . --cura_conan_version $CURA_CONAN_VERSION --filename "${{ steps.filename.outputs.INSTALLER_FILENAME }}" --build_dmg --build_pkg --app_name "$CURA_APP_NAME"
+ working-directory: dist
+
+ - name: Upload the dmg
+ uses: actions/upload-artifact@v3
+ with:
+ name: ${{ steps.filename.outputs.INSTALLER_FILENAME }}-dmg
+ path: |
+ dist/${{ steps.filename.outputs.INSTALLER_FILENAME }}.dmg
+ retention-days: 5
+
+ - name: Upload the pkg
+ uses: actions/upload-artifact@v3
+ with:
+ name: ${{ steps.filename.outputs.INSTALLER_FILENAME }}-pkg
+ path: |
+ dist/${{ steps.filename.outputs.INSTALLER_FILENAME }}.pkg
+ retention-days: 5
+
+
+ notify-export:
+ if: ${{ always() }}
+ needs: [ cura-installer-create ]
+
+ uses: ultimaker/cura/.github/workflows/notify.yml@main
+ with:
+ success: ${{ contains(join(needs.*.result, ','), 'success') }}
+ success_title: "Create the Cura distributions"
+ success_body: "Installers for ${{ inputs.cura_conan_version }}"
+ failure_title: "Failed to create the Cura distributions"
+ failure_body: "Failed to create at least 1 installer for ${{ inputs.cura_conan_version }}"
+ secrets: inherit
diff --git a/.github/workflows/requirements-conan-package.txt b/.github/workflows/requirements-conan-package.txt
index 26d167db2b5..6b4d4cffc88 100644
--- a/.github/workflows/requirements-conan-package.txt
+++ b/.github/workflows/requirements-conan-package.txt
@@ -1,2 +1,2 @@
-conan==1.56.0
+conan==1.60.2
sip
diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml
new file mode 100644
index 00000000000..df9056d4540
--- /dev/null
+++ b/.github/workflows/windows.yml
@@ -0,0 +1,270 @@
+name: Windows Installer
+run-name: ${{ inputs.cura_conan_version }} for Windows-${{ inputs.architecture }} by @${{ github.actor }}
+
+on:
+ workflow_dispatch:
+ inputs:
+ cura_conan_version:
+ description: 'Cura Conan Version'
+ default: 'cura/latest@ultimaker/testing'
+ required: true
+ type: string
+ conan_args:
+ description: 'Conan args: eq.: --require-override'
+ default: ''
+ required: false
+ type: string
+ enterprise:
+ description: 'Build Cura as an Enterprise edition'
+ default: false
+ required: true
+ type: boolean
+ staging:
+ description: 'Use staging API'
+ default: false
+ required: true
+ type: boolean
+ architecture:
+ description: 'Architecture'
+ required: true
+ default: 'X64'
+ type: choice
+ options:
+ - X64
+ operating_system:
+ description: 'OS'
+ required: true
+ default: 'windows-2022'
+ type: choice
+ options:
+ - windows-2022
+ workflow_call:
+ inputs:
+ cura_conan_version:
+ description: 'Cura Conan Version'
+ default: 'cura/latest@ultimaker/testing'
+ required: true
+ type: string
+ conan_args:
+ description: 'Conan args: eq.: --require-override'
+ default: ''
+ required: false
+ type: string
+ enterprise:
+ description: 'Build Cura as an Enterprise edition'
+ default: false
+ required: true
+ type: boolean
+ staging:
+ description: 'Use staging API'
+ default: false
+ required: true
+ type: boolean
+ architecture:
+ description: 'Architecture'
+ required: true
+ default: 'X64'
+ type: string
+ operating_system:
+ description: 'OS'
+ required: true
+ default: 'windows-2022'
+ type: string
+
+env:
+ CONAN_LOGIN_USERNAME_CURA: ${{ secrets.CONAN_USER }}
+ CONAN_PASSWORD_CURA: ${{ secrets.CONAN_PASS }}
+ WIN_CERT_INSTALLER_CER: ${{ secrets.WIN_CERT_INSTALLER_CER }}
+ WIN_CERT_INSTALLER_CER_PASS: ${{ secrets.WIN_CERT_INSTALLER_CER_PASS }}
+ CURA_CONAN_VERSION: ${{ inputs.cura_conan_version }}
+ ENTERPRISE: ${{ inputs.enterprise }}
+ STAGING: ${{ inputs.staging }}
+
+jobs:
+ cura-installer-create:
+ runs-on: ${{ inputs.operating_system }}
+
+ outputs:
+ INSTALLER_FILENAME: ${{ steps.filename.outputs.INSTALLER_FILENAME }}
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: Setup Python and pip
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.10.x'
+ cache: 'pip'
+ cache-dependency-path: .github/workflows/requirements-conan-package.txt
+
+ - name: Install Python requirements for runner
+ run: pip install -r .github/workflows/requirements-conan-package.txt
+
+ - name: Cache Conan local repository packages (Powershell)
+ uses: actions/cache@v3
+ with:
+ path: |
+ C:\Users\runneradmin\.conan\data
+ C:\.conan
+ C:\Users\runneradmin\.conan\conan_download_cache
+ key: conan-${{ runner.os }}-${{ runner.arch }}-installer-cache
+
+ - name: Create the default Conan profile
+ run: conan profile new default --detect --force
+
+ - name: Get Conan configuration
+ run: |
+ conan config install https://github.com/Ultimaker/conan-config.git
+ conan config install https://github.com/Ultimaker/conan-config.git -a "-b runner/${{ runner.os }}/${{ runner.arch }}"
+
+ - name: Use Conan download cache (Powershell)
+ run: conan config set storage.download_cache="C:\Users\runneradmin\.conan\conan_download_cache"
+
+ - name: Create the Packages (Powershell)
+ run: conan install $Env:CURA_CONAN_VERSION ${{ inputs.conan_args }} --build=missing --update -if cura_inst -g VirtualPythonEnv -o cura:enterprise=$Env:ENTERPRISE -o cura:staging=$Env:STAGING --json "cura_inst/conan_install_info.json"
+
+ - name: Upload the Package(s)
+ if: always()
+ run: |
+ conan upload "*" -r cura --all -c
+
+ - name: Set Environment variables for Cura (Powershell)
+ run: |
+ echo "${Env:WIX}\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
+ .\cura_inst\Scripts\activate_github_actions_env.ps1
+ .\cura_inst\Scripts\activate_github_actions_version_env.ps1
+
+ - name: Install OpenSSL shared
+ run: conan install openssl/1.1.1l@_/_ --build=missing --update -o openssl:shared=True -g deploy
+
+ - name: Copy OpenSSL shared (Powershell)
+ run: |
+ cp openssl/bin/*.dll ./cura_inst/Scripts/
+ cp openssl/lib/*.lib ./cura_inst/Lib/
+
+ - name: Create the Cura dist
+ run: pyinstaller ./cura_inst/UltiMaker-Cura.spec
+
+ - name: Output the name file name and extension
+ id: filename
+ shell: python
+ run: |
+ import os
+ enterprise = "-Enterprise" if "${{ inputs.enterprise }}" == "true" else ""
+ installer_filename = f"UltiMaker-Cura-{os.getenv('CURA_VERSION_FULL')}{enterprise}-win64-${{ inputs.architecture }}"
+ output_env = os.environ["GITHUB_OUTPUT"]
+ content = ""
+ if os.path.exists(output_env):
+ with open(output_env, "r") as f:
+ content = f.read()
+ with open(output_env, "w") as f:
+ f.write(content)
+ f.writelines(f"INSTALLER_FILENAME={installer_filename}\n")
+
+ - name: Summarize the used Conan dependencies
+ shell: python
+ run: |
+ import os
+ import json
+ from pathlib import Path
+
+ conan_install_info_path = Path("cura_inst/conan_install_info.json")
+ conan_info = {"installed": []}
+ if os.path.exists(conan_install_info_path):
+ with open(conan_install_info_path, "r") as f:
+ conan_info = json.load(f)
+ sorted_deps = sorted([dep["recipe"]["id"].replace('#', r' rev: ') for dep in conan_info["installed"]])
+
+ summary_env = os.environ["GITHUB_STEP_SUMMARY"]
+ content = ""
+ if os.path.exists(summary_env):
+ with open(summary_env, "r") as f:
+ content = f.read()
+
+ with open(summary_env, "w") as f:
+ f.write(content)
+ f.writelines("# ${{ steps.filename.outputs.INSTALLER_FILENAME }}\n")
+ f.writelines("## Conan packages:\n")
+ for dep in sorted_deps:
+ f.writelines(f"`{dep}`\n")
+
+ - name: Summarize the used Python modules
+ shell: python
+ run: |
+ import os
+ import pkg_resources
+ summary_env = os.environ["GITHUB_STEP_SUMMARY"]
+ content = ""
+ if os.path.exists(summary_env):
+ with open(summary_env, "r") as f:
+ content = f.read()
+
+ with open(summary_env, "w") as f:
+ f.write(content)
+ f.writelines("## Python modules:\n")
+ for package in pkg_resources.working_set:
+ f.writelines(f"`{package.key}/{package.version}`\n")
+
+ - name: Create PFX certificate from BASE64_PFX_CONTENT secret
+ id: create-pfx
+ env:
+ PFX_CONTENT: ${{ secrets.WIN_CERT_INSTALLER_CER }}
+ run: |
+ $pfxPath = Join-Path -Path $env:RUNNER_TEMP -ChildPath "cert.pfx";
+ $encodedBytes = [System.Convert]::FromBase64String($env:PFX_CONTENT);
+ Set-Content $pfxPath -Value $encodedBytes -AsByteStream;
+ echo "PFX_PATH=$pfxPath" >> $env:GITHUB_OUTPUT;
+
+ - name: Create the Windows msi installer (Powershell)
+ run: |
+ python ..\cura_inst\packaging\msi\create_windows_msi.py ..\cura_inst .\UltiMaker-Cura "${{steps.filename.outputs.INSTALLER_FILENAME }}.msi" "$Env:CURA_APP_NAME"
+ working-directory: dist
+
+ - name: Sign the Windows msi installer (Powershell)
+ env:
+ PFX_PATH: ${{ steps.create-pfx.outputs.PFX_PATH }}
+ run: |
+ & "C:/Program Files (x86)/Windows Kits/10/bin/10.0.17763.0/x86/signtool.exe" sign /f $Env:PFX_PATH /p "$Env:WIN_CERT_INSTALLER_CER_PASS" /fd SHA256 /t http://timestamp.digicert.com "${{steps.filename.outputs.INSTALLER_FILENAME }}.msi"
+ working-directory: dist
+
+ - name: Create the Windows exe installer (Powershell)
+ run: |
+ python ..\cura_inst\packaging\NSIS\create_windows_installer.py ../cura_inst . "${{steps.filename.outputs.INSTALLER_FILENAME }}.exe"
+ working-directory: dist
+
+ - name: Sign the Windows exe installer (Powershell)
+ env:
+ PFX_PATH: ${{ steps.create-pfx.outputs.PFX_PATH }}
+ run: |
+ & "C:/Program Files (x86)/Windows Kits/10/bin/10.0.17763.0/x86/signtool.exe" sign /f $Env:PFX_PATH /p "$Env:WIN_CERT_INSTALLER_CER_PASS" /fd SHA256 /t http://timestamp.digicert.com "${{steps.filename.outputs.INSTALLER_FILENAME }}.exe"
+ working-directory: dist
+
+ - name: Upload the msi
+ uses: actions/upload-artifact@v3
+ with:
+ name: ${{steps.filename.outputs.INSTALLER_FILENAME }}-msi
+ path: |
+ dist/${{steps.filename.outputs.INSTALLER_FILENAME }}.msi
+ retention-days: 5
+
+ - name: Upload the exe
+ uses: actions/upload-artifact@v3
+ with:
+ name: ${{steps.filename.outputs.INSTALLER_FILENAME }}-exe
+ path: |
+ dist/${{steps.filename.outputs.INSTALLER_FILENAME }}.exe
+ retention-days: 5
+
+ notify-export:
+ if: ${{ always() }}
+ needs: [ cura-installer-create ]
+
+ uses: ultimaker/cura/.github/workflows/notify.yml@main
+ with:
+ success: ${{ contains(join(needs.*.result, ','), 'success') }}
+ success_title: "Create the Cura distributions"
+ success_body: "Installers for ${{ inputs.cura_conan_version }}"
+ failure_title: "Failed to create the Cura distributions"
+ failure_body: "Failed to create at least 1 installer for ${{ inputs.cura_conan_version }}"
+ secrets: inherit
diff --git a/CITATION.cff b/CITATION.cff
index 627cfa2515a..7a93bbf0990 100644
--- a/CITATION.cff
+++ b/CITATION.cff
@@ -18,8 +18,8 @@ url: "https://ultimaker.com/software/ultimaker-cura"
repository-code: "https://github.com/Ultimaker/Cura"
license: LGPL-3.0
license-url: "https://github.com/Ultimaker/Cura/blob/main/LICENSE"
-version: 5.2.1
-date-released: "2022-10-19"
+version: 5.4.0
+date-released: "2023-07-04"
keywords:
- Ultimaker
- Cura
diff --git a/FUNDING.yml b/FUNDING.yml
new file mode 100644
index 00000000000..2d108a74e15
--- /dev/null
+++ b/FUNDING.yml
@@ -0,0 +1 @@
+github: [ultimaker]
diff --git a/README.md b/README.md
index c40ead2baa6..eb047993397 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@
[![Badge Test]][Test]   
[![Badge Conan]][Conan]   
-
+![Badge Downloads]
@@ -84,6 +84,7 @@
[Badge Conan]: https://img.shields.io/github/workflow/status/Ultimaker/Cura/conan-package?style=for-the-badge&logoColor=white&labelColor=6185aa&color=4c6987&logo=Conan&label=Conan%20Package
[Badge Test]: https://img.shields.io/github/workflow/status/Ultimaker/Cura/unit-test?style=for-the-badge&logoColor=white&labelColor=4a999d&color=346c6e&logo=Codacy&label=Unit%20Test
[Badge Size]: https://img.shields.io/github/repo-size/ultimaker/cura?style=for-the-badge&logoColor=white&labelColor=715a97&color=584674&logo=GoogleAnalytics
+[Badge Downloads]: https://img.shields.io/github/downloads-pre/Ultimaker/Cura/latest/total?style=for-the-badge
diff --git a/conanfile.py b/conanfile.py
index d4f365c7a42..77cecf1134b 100644
--- a/conanfile.py
+++ b/conanfile.py
@@ -466,6 +466,7 @@ def deploy(self):
save(self, os.path.join(self._script_dir, f"activate_github_actions_version_env{ext}"), activate_github_actions_version_env)
self._generate_cura_version(os.path.join(self._site_packages, "cura"))
+ self._generate_about_versions(str(self._share_dir.joinpath("cura", "resources", "qml", "Dialogs")))
entitlements_file = "'{}'".format(Path(self.cpp_info.res_paths[2], "MacOS", "cura.entitlements"))
self._generate_pyinstaller_spec(location = self._base_dir,
@@ -473,7 +474,6 @@ def deploy(self):
icon_path = "'{}'".format(os.path.join(self.package_folder, self.cpp_info.resdirs[2], self.conan_data["pyinstaller"]["icon"][str(self.settings.os)])).replace("\\", "\\\\"),
entitlements_file = entitlements_file if self.settings.os == "Macos" else "None")
- self._generate_about_versions(os.path.join(self.source_folder, "resources", "qml", "Dialogs"))
def package(self):
copy(self, "cura_app.py", src = self.source_folder, dst = os.path.join(self.package_folder, self.cpp.package.bindirs[0]))
diff --git a/cura/CuraActions.py b/cura/CuraActions.py
index 193803325f4..6c2d3f4cb8d 100644
--- a/cura/CuraActions.py
+++ b/cura/CuraActions.py
@@ -1,15 +1,18 @@
-# Copyright (c) 2018 Ultimaker B.V.
+# Copyright (c) 2023 UltiMaker
# Cura is released under the terms of the LGPLv3 or higher.
-from PyQt6.QtCore import QObject, QUrl
-from PyQt6.QtGui import QDesktopServices
from typing import List, cast
+from PyQt6.QtCore import QObject, QUrl, QMimeData
+from PyQt6.QtGui import QDesktopServices
+from PyQt6.QtWidgets import QApplication
+
from UM.Event import CallFunctionEvent
from UM.FlameProfiler import pyqtSlot
from UM.Math.Vector import Vector
from UM.Scene.Selection import Selection
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
+from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Operations.GroupedOperation import GroupedOperation
from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
from UM.Operations.TranslateOperation import TranslateOperation
@@ -19,6 +22,7 @@
from cura.MultiplyObjectsJob import MultiplyObjectsJob
from cura.Settings.SetObjectExtruderOperation import SetObjectExtruderOperation
from cura.Settings.ExtruderManager import ExtruderManager
+from cura.Arranging.Nest2DArrange import createGroupOperationForArrange
from cura.Operations.SetBuildPlateNumberOperation import SetBuildPlateNumberOperation
@@ -181,5 +185,60 @@ def setBuildPlateForSelection(self, build_plate_nr: int) -> None:
Selection.clear()
+ @pyqtSlot()
+ def cut(self) -> None:
+ self.copy()
+ self.deleteSelection()
+
+ @pyqtSlot()
+ def copy(self) -> None:
+ mesh_writer = cura.CuraApplication.CuraApplication.getInstance().getMeshFileHandler().getWriter("3MFWriter")
+ if not mesh_writer:
+ Logger.log("e", "No 3MF writer found, unable to copy.")
+ return
+
+ # Get the selected nodes
+ selected_objects = Selection.getAllSelectedObjects()
+ # Serialize the nodes to a string
+ scene_string = mesh_writer.sceneNodesToString(selected_objects)
+ # Put the string on the clipboard
+ QApplication.clipboard().setText(scene_string)
+
+ @pyqtSlot()
+ def paste(self) -> None:
+ application = cura.CuraApplication.CuraApplication.getInstance()
+ mesh_reader = application.getMeshFileHandler().getReaderForFile(".3mf")
+ if not mesh_reader:
+ Logger.log("e", "No 3MF reader found, unable to paste.")
+ return
+
+ # Parse the scene from the clipboard
+ scene_string = QApplication.clipboard().text()
+
+ nodes = mesh_reader.stringToSceneNodes(scene_string)
+
+ if not nodes:
+ # Nothing to paste
+ return
+
+ # Find all fixed nodes, these are the nodes that should be avoided when arranging
+ fixed_nodes = []
+ root = application.getController().getScene().getRoot()
+ for node in DepthFirstIterator(root):
+ # Only count sliceable objects
+ if node.callDecoration("isSliceable"):
+ fixed_nodes.append(node)
+ # Add the new nodes to the scene, and arrange them
+ group_operation, not_fit_count = createGroupOperationForArrange(nodes, application.getBuildVolume(),
+ fixed_nodes, factor=10000,
+ add_new_nodes_in_scene=True)
+ group_operation.push()
+
+ # deselect currently selected nodes, and select the new nodes
+ for node in Selection.getAllSelectedObjects():
+ Selection.remove(node)
+ for node in nodes:
+ Selection.add(node)
+
def _openUrl(self, url: QUrl) -> None:
QDesktopServices.openUrl(url)
diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py
index 889e442eaa5..f44393657f2 100755
--- a/cura/CuraApplication.py
+++ b/cura/CuraApplication.py
@@ -409,7 +409,7 @@ def _initializeSettingFunctions(self):
SettingFunction.registerOperator("extruderValue", self._cura_formula_functions.getValueInExtruder)
SettingFunction.registerOperator("extruderValues", self._cura_formula_functions.getValuesInAllExtruders)
- SettingFunction.registerOperator("anyExtruderNrWithOrDefault", self._cura_formula_functions.getAnyExtruderPositionWithOrDefault)
+ SettingFunction.registerOperator("anyExtruderWithMaterial", self._cura_formula_functions.getExtruderPositionWithMaterial)
SettingFunction.registerOperator("resolveOrValue", self._cura_formula_functions.getResolveOrValue)
SettingFunction.registerOperator("defaultExtruderPosition", self._cura_formula_functions.getDefaultExtruderPosition)
SettingFunction.registerOperator("valueFromContainer", self._cura_formula_functions.getValueFromContainerAtIndex)
@@ -495,41 +495,45 @@ def __setLatestResouceVersionsForVersionUpgrade(self):
def startSplashWindowPhase(self) -> None:
"""Runs preparations that needs to be done before the starting process."""
- super().startSplashWindowPhase()
-
- if not self.getIsHeadLess():
- try:
- self.setWindowIcon(QIcon(Resources.getPath(Resources.Images, "cura-icon.png" if not ApplicationMetadata.IsAlternateVersion else "cura-icon_wip.png")))
- except FileNotFoundError:
- Logger.log("w", "Unable to find the window icon.")
-
self.setRequiredPlugins([
# Misc.:
- "ConsoleLogger", #You want to be able to read the log if something goes wrong.
- "CuraEngineBackend", #Cura is useless without this one since you can't slice.
- "FileLogger", #You want to be able to read the log if something goes wrong.
- "XmlMaterialProfile", #Cura crashes without this one.
- "Marketplace", #This contains the interface to enable/disable plug-ins, so if you disable it you can't enable it back.
- "PrepareStage", #Cura is useless without this one since you can't load models.
- "PreviewStage", #This shows the list of the plugin views that are installed in Cura.
- "MonitorStage", #Major part of Cura's functionality.
- "LocalFileOutputDevice", #Major part of Cura's functionality.
- "LocalContainerProvider", #Cura is useless without any profiles or setting definitions.
+ "ConsoleLogger", # You want to be able to read the log if something goes wrong.
+ "CuraEngineBackend", # Cura is useless without this one since you can't slice.
+ "FileLogger", # You want to be able to read the log if something goes wrong.
+ "XmlMaterialProfile", # Cura crashes without this one.
+ "Marketplace",
+ # This contains the interface to enable/disable plug-ins, so if you disable it you can't enable it back.
+ "PrepareStage", # Cura is useless without this one since you can't load models.
+ "PreviewStage", # This shows the list of the plugin views that are installed in Cura.
+ "MonitorStage", # Major part of Cura's functionality.
+ "LocalFileOutputDevice", # Major part of Cura's functionality.
+ "LocalContainerProvider", # Cura is useless without any profiles or setting definitions.
# Views:
- "SimpleView", #Dependency of SolidView.
- "SolidView", #Displays models. Cura is useless without it.
+ "SimpleView", # Dependency of SolidView.
+ "SolidView", # Displays models. Cura is useless without it.
# Readers & Writers:
- "GCodeWriter", #Cura is useless if it can't write its output.
- "STLReader", #Most common model format, so disabling this makes Cura 90% useless.
- "3MFWriter", #Required for writing project files.
+ "GCodeWriter", # Cura is useless if it can't write its output.
+ "STLReader", # Most common model format, so disabling this makes Cura 90% useless.
+ "3MFWriter", # Required for writing project files.
# Tools:
- "CameraTool", #Needed to see the scene. Cura is useless without it.
- "SelectionTool", #Dependency of the rest of the tools.
- "TranslateTool", #You'll need this for almost every print.
+ "CameraTool", # Needed to see the scene. Cura is useless without it.
+ "SelectionTool", # Dependency of the rest of the tools.
+ "TranslateTool", # You'll need this for almost every print.
])
+ # Plugins need to be set here, since in the super the check is done if they are actually loaded.
+
+ super().startSplashWindowPhase()
+
+ if not self.getIsHeadLess():
+ try:
+ self.setWindowIcon(QIcon(Resources.getPath(Resources.Images, "cura-icon.png" if not ApplicationMetadata.IsAlternateVersion else "cura-icon_wip.png")))
+ except FileNotFoundError:
+ Logger.log("w", "Unable to find the window icon.")
+
+
self._i18n_catalog = i18nCatalog("cura")
self._update_platform_activity_timer = QTimer()
diff --git a/cura/Scene/ConvexHullDecorator.py b/cura/Scene/ConvexHullDecorator.py
index 06ec247ae4a..0dbf3ba7829 100644
--- a/cura/Scene/ConvexHullDecorator.py
+++ b/cura/Scene/ConvexHullDecorator.py
@@ -111,11 +111,7 @@ def getConvexHull(self) -> Optional[Polygon]:
# Parent can be None if node is just loaded.
if self._isSingularOneAtATimeNode():
- hull = self.getConvexHullHeadFull()
- if hull is None:
- return None
- hull = self._add2DAdhesionMargin(hull)
- return hull
+ return self.getConvexHullHeadFull()
return self._compute2DConvexHull()
@@ -323,6 +319,7 @@ def _getHeadAndFans(self) -> Polygon:
def _compute2DConvexHeadFull(self) -> Optional[Polygon]:
convex_hull = self._compute2DConvexHull()
+ convex_hull = self._add2DAdhesionMargin(convex_hull)
if convex_hull:
return convex_hull.getMinkowskiHull(self._getHeadAndFans())
return None
diff --git a/cura/Settings/CuraFormulaFunctions.py b/cura/Settings/CuraFormulaFunctions.py
index fd6555e679d..08c24180bbc 100644
--- a/cura/Settings/CuraFormulaFunctions.py
+++ b/cura/Settings/CuraFormulaFunctions.py
@@ -97,14 +97,14 @@ def getValuesInAllExtruders(self, property_key: str,
return result
- # Get the first extruder that adheres to a specific (boolean) property, like 'material_is_support_material'.
- def getAnyExtruderPositionWithOrDefault(self, filter_key: str,
+ # Get the first extruder with material that adheres to a specific (boolean) property, like 'material_is_support_material'.
+ def getExtruderPositionWithMaterial(self, filter_key: str,
context: Optional["PropertyEvaluationContext"] = None) -> str:
for extruder in self._getActiveExtruders(context):
- value = extruder.getRawProperty(filter_key, "value", context=context)
- if value is None or not value:
- continue
- return str(extruder.position)
+ material_container = extruder.material
+ value = material_container.getProperty(filter_key, "value", context)
+ if value is not None:
+ return str(extruder.position)
return self.getDefaultExtruderPosition()
# Get the resolve value or value for a given key.
diff --git a/docs/profiles/getting_a_setting_value.md b/docs/profiles/getting_a_setting_value.md
index bd106eb2da4..ab18be0f42f 100644
--- a/docs/profiles/getting_a_setting_value.md
+++ b/docs/profiles/getting_a_setting_value.md
@@ -54,7 +54,7 @@ There are also a few extra things that can be used in these expressions:
* The function `extruderValue(extruder, key)` will evaluate a particular setting for a particular extruder.
* The function `resolveOrValue(key)` will perform the full setting evaluation as described in this document for the current context (so if this setting is being evaluated for the second extruder it would perform it as if coming from the second extruder).
* The function `defaultExtruderPosition()` will get the first extruder that is not disabled. For instance, if a printer has three extruders but the first is disabled, this would return `1` to indicate the second extruder (0-indexed).
-* The function `anyExtruderNrWithOrDefault(key)` will filter the list of extruders on the key, and then give the first index for which it is true, or if none of them are, the default one as specified by the 'default extruder position' function above.
+* The function `anyExtruderWithMaterial(key)` will filter the list of extruders on the key of material quality, and then give the first index for which it is true, or if none of them are, the default one as specified by the 'default extruder position' function above.
* The function `valueFromContainer(key, index)` will get a setting value from the global stack, but skip the first few containers in that stack. It will skip until it reaches a particular index in the container stack.
* The function `extruderValueFromContainer(key, index)` will get a setting value from the current extruder stack, but skip the first few containers in that stack. It will skip until it reaches a particular index in the container stack.
diff --git a/packaging/MacOS/build_macos.py b/packaging/MacOS/build_macos.py
index dcde6299000..eae9afceff7 100644
--- a/packaging/MacOS/build_macos.py
+++ b/packaging/MacOS/build_macos.py
@@ -21,6 +21,7 @@ def build_dmg(source_path: str, dist_path: str, filename: str, app_name: str) ->
"--icon", app_name, "169", "272",
"--eula", f"{source_path}/packaging/cura_license.txt",
"--background", f"{source_path}/packaging/MacOs/cura_background_dmg.png",
+ "--hdiutil-quiet",
f"{dist_path}/{filename}",
f"{dist_path}/{app_name}"]
@@ -138,18 +139,20 @@ def create_dmg(filename: str, dist_path: str, source_path: str, app_name: str) -
if __name__ == "__main__":
parser = argparse.ArgumentParser(description = "Create installer for Cura.")
- parser.add_argument("source_path", type = str, help = "Path to Pyinstaller source folder")
- parser.add_argument("dist_path", type = str, help = "Path to Pyinstaller dist folder")
- parser.add_argument("cura_conan_version", type = str, help="The version of cura")
- parser.add_argument("filename", type = str, help = "Filename of the pkg/dmg (e.g. 'UltiMaker-Cura-5.1.0-beta-Macos-X64.pkg' or 'UltiMaker-Cura-5.1.0-beta-Macos-X64.dmg')")
- parser.add_argument("app_name", type = str, help = "Filename of the .app that will be contained within the dmg/pkg")
+ parser.add_argument("--source_path", required = True, type = str, help = "Path to Pyinstaller source folder")
+ parser.add_argument("--dist_path", required = True, type = str, help = "Path to Pyinstaller dist folder")
+ parser.add_argument("--cura_conan_version", required = True, type = str, help = "The version of cura")
+ parser.add_argument("--filename", required = True, type = str, help = "Filename of the pkg/dmg (e.g. 'UltiMaker-Cura-5.5.0-Macos-X64' or 'UltiMaker-Cura-5.5.0-beta.1-Macos-ARM64')")
+ parser.add_argument("--build_pkg", action="store_true", default = False, help = "build the pkg")
+ parser.add_argument("--build_dmg", action="store_true", default = True, help = "build the dmg")
+ parser.add_argument("--app_name", required = True, type = str, help = "Filename of the .app that will be contained within the dmg/pkg")
args = parser.parse_args()
cura_version = args.cura_conan_version.split("/")[-1]
app_name = f"{args.app_name}.app"
- if Path(args.filename).suffix == ".pkg":
- create_pkg_installer(args.filename, args.dist_path, cura_version, app_name)
- else:
- create_dmg(args.filename, args.dist_path, args.source_path, app_name)
+ if args.build_pkg:
+ create_pkg_installer(f"{args.filename}.pkg", args.dist_path, cura_version, app_name)
+ if args.build_dmg:
+ create_dmg(f"{args.filename}.dmg", args.dist_path, args.source_path, app_name)
diff --git a/plugins/3MFReader/ThreeMFReader.py b/plugins/3MFReader/ThreeMFReader.py
index e8b6a54e46d..e06e9dcf4e4 100755
--- a/plugins/3MFReader/ThreeMFReader.py
+++ b/plugins/3MFReader/ThreeMFReader.py
@@ -56,7 +56,8 @@ def __init__(self) -> None:
def emptyFileHintSet(self) -> bool:
return self._empty_project
- def _createMatrixFromTransformationString(self, transformation: str) -> Matrix:
+ @staticmethod
+ def _createMatrixFromTransformationString(transformation: str) -> Matrix:
if transformation == "":
return Matrix()
@@ -90,7 +91,8 @@ def _createMatrixFromTransformationString(self, transformation: str) -> Matrix:
return temp_mat
- def _convertSavitarNodeToUMNode(self, savitar_node: Savitar.SceneNode, file_name: str = "") -> Optional[SceneNode]:
+ @staticmethod
+ def _convertSavitarNodeToUMNode(savitar_node: Savitar.SceneNode, file_name: str = "") -> Optional[SceneNode]:
"""Convenience function that converts a SceneNode object (as obtained from libSavitar) to a scene node.
:returns: Scene node.
@@ -119,7 +121,7 @@ def _convertSavitarNodeToUMNode(self, savitar_node: Savitar.SceneNode, file_name
pass
um_node.setName(node_name)
um_node.setId(node_id)
- transformation = self._createMatrixFromTransformationString(savitar_node.getTransformation())
+ transformation = ThreeMFReader._createMatrixFromTransformationString(savitar_node.getTransformation())
um_node.setTransformation(transformation)
mesh_builder = MeshBuilder()
@@ -138,7 +140,7 @@ def _convertSavitarNodeToUMNode(self, savitar_node: Savitar.SceneNode, file_name
um_node.setMeshData(mesh_data)
for child in savitar_node.getChildren():
- child_node = self._convertSavitarNodeToUMNode(child)
+ child_node = ThreeMFReader._convertSavitarNodeToUMNode(child)
if child_node:
um_node.addChild(child_node)
@@ -214,7 +216,7 @@ def _read(self, file_name: str) -> Union[SceneNode, List[SceneNode]]:
CuraApplication.getInstance().getController().getScene().setMetaDataEntry(key, value)
for node in scene_3mf.getSceneNodes():
- um_node = self._convertSavitarNodeToUMNode(node, file_name)
+ um_node = ThreeMFReader._convertSavitarNodeToUMNode(node, file_name)
if um_node is None:
continue
@@ -300,8 +302,23 @@ def _getScaleFromUnit(self, unit: Optional[str]) -> Vector:
if unit is None:
unit = "millimeter"
elif unit not in conversion_to_mm:
- Logger.log("w", "Unrecognised unit {unit} used. Assuming mm instead.".format(unit = unit))
+ Logger.log("w", "Unrecognised unit {unit} used. Assuming mm instead.".format(unit=unit))
unit = "millimeter"
scale = conversion_to_mm[unit]
return Vector(scale, scale, scale)
+
+ @staticmethod
+ def stringToSceneNodes(scene_string: str) -> List[SceneNode]:
+ parser = Savitar.ThreeMFParser()
+ scene = parser.parse(scene_string)
+
+ # Convert the scene to scene nodes
+ nodes = []
+ for savitar_node in scene.getSceneNodes():
+ scene_node = ThreeMFReader._convertSavitarNodeToUMNode(savitar_node, "file_name")
+ if scene_node is None:
+ continue
+ nodes.append(scene_node)
+
+ return nodes
diff --git a/plugins/3MFWriter/ThreeMFWriter.py b/plugins/3MFWriter/ThreeMFWriter.py
index 57c667145e3..3f6fef7201d 100644
--- a/plugins/3MFWriter/ThreeMFWriter.py
+++ b/plugins/3MFWriter/ThreeMFWriter.py
@@ -55,11 +55,12 @@ def __init__(self):
"cura": "http://software.ultimaker.com/xml/cura/3mf/2015/10"
}
- self._unit_matrix_string = self._convertMatrixToString(Matrix())
+ self._unit_matrix_string = ThreeMFWriter._convertMatrixToString(Matrix())
self._archive: Optional[zipfile.ZipFile] = None
self._store_archive = False
- def _convertMatrixToString(self, matrix):
+ @staticmethod
+ def _convertMatrixToString(matrix):
result = ""
result += str(matrix._data[0, 0]) + " "
result += str(matrix._data[1, 0]) + " "
@@ -83,7 +84,8 @@ def setStoreArchive(self, store_archive):
"""
self._store_archive = store_archive
- def _convertUMNodeToSavitarNode(self, um_node, transformation = Matrix()):
+ @staticmethod
+ def _convertUMNodeToSavitarNode(um_node, transformation=Matrix()):
"""Convenience function that converts an Uranium SceneNode object to a SavitarSceneNode
:returns: Uranium Scene node.
@@ -100,7 +102,7 @@ def _convertUMNodeToSavitarNode(self, um_node, transformation = Matrix()):
node_matrix = um_node.getLocalTransformation()
- matrix_string = self._convertMatrixToString(node_matrix.preMultiply(transformation))
+ matrix_string = ThreeMFWriter._convertMatrixToString(node_matrix.preMultiply(transformation))
savitar_node.setTransformation(matrix_string)
mesh_data = um_node.getMeshData()
@@ -133,7 +135,7 @@ def _convertUMNodeToSavitarNode(self, um_node, transformation = Matrix()):
# only save the nodes on the active build plate
if child_node.callDecoration("getBuildPlateNumber") != active_build_plate_nr:
continue
- savitar_child_node = self._convertUMNodeToSavitarNode(child_node)
+ savitar_child_node = ThreeMFWriter._convertUMNodeToSavitarNode(child_node)
if savitar_child_node is not None:
savitar_node.addChild(savitar_child_node)
@@ -221,7 +223,7 @@ def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode) -> bool:
for node in nodes:
if node == root_node:
for root_child in node.getChildren():
- savitar_node = self._convertUMNodeToSavitarNode(root_child, transformation_matrix)
+ savitar_node = ThreeMFWriter._convertUMNodeToSavitarNode(root_child, transformation_matrix)
if savitar_node:
savitar_scene.addSceneNode(savitar_node)
else:
@@ -303,9 +305,19 @@ def _createSnapshot(self):
Logger.log("w", "Can't create snapshot when renderer not initialized.")
return None
try:
- snapshot = Snapshot.snapshot(width = 300, height = 300)
+ snapshot = Snapshot.snapshot(width=300, height=300)
except:
Logger.logException("w", "Failed to create snapshot image")
return None
return snapshot
+
+ @staticmethod
+ def sceneNodesToString(scene_nodes: [SceneNode]) -> str:
+ savitar_scene = Savitar.Scene()
+ for scene_node in scene_nodes:
+ savitar_node = ThreeMFWriter._convertUMNodeToSavitarNode(scene_node)
+ savitar_scene.addSceneNode(savitar_node)
+ parser = Savitar.ThreeMFParser()
+ scene_string = parser.sceneToString(savitar_scene)
+ return scene_string
diff --git a/plugins/PostProcessingPlugin/PostProcessingPlugin.py b/plugins/PostProcessingPlugin/PostProcessingPlugin.py
index fbb42140213..b3ef761af5e 100644
--- a/plugins/PostProcessingPlugin/PostProcessingPlugin.py
+++ b/plugins/PostProcessingPlugin/PostProcessingPlugin.py
@@ -1,5 +1,5 @@
# Copyright (c) 2018 Jaime van Kessel, Ultimaker B.V.
-# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
+# The PostProcessingPlugin is released under the terms of the LGPLv3 or higher.
import configparser # The script lists are stored in metadata as serialised config files.
import importlib.util
diff --git a/plugins/PostProcessingPlugin/PostProcessingPlugin.qml b/plugins/PostProcessingPlugin/PostProcessingPlugin.qml
index a80f304aaac..0f379479bac 100644
--- a/plugins/PostProcessingPlugin/PostProcessingPlugin.qml
+++ b/plugins/PostProcessingPlugin/PostProcessingPlugin.qml
@@ -1,5 +1,5 @@
// Copyright (c) 2022 Jaime van Kessel, Ultimaker B.V.
-// The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
+// The PostProcessingPlugin is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2
import QtQuick.Controls 2.15
diff --git a/plugins/PostProcessingPlugin/Script.py b/plugins/PostProcessingPlugin/Script.py
index 1cc9b59c9c3..be661e08894 100644
--- a/plugins/PostProcessingPlugin/Script.py
+++ b/plugins/PostProcessingPlugin/Script.py
@@ -1,6 +1,6 @@
# Copyright (c) 2015 Jaime van Kessel
# Copyright (c) 2018 Ultimaker B.V.
-# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
+# The PostProcessingPlugin is released under the terms of the LGPLv3 or higher.
from typing import Optional, Any, Dict, TYPE_CHECKING, List
from UM.Signal import Signal, signalemitter
diff --git a/plugins/PostProcessingPlugin/scripts/ChangeAtZ.py b/plugins/PostProcessingPlugin/scripts/ChangeAtZ.py
index 91b36389f3f..2930623a933 100644
--- a/plugins/PostProcessingPlugin/scripts/ChangeAtZ.py
+++ b/plugins/PostProcessingPlugin/scripts/ChangeAtZ.py
@@ -1,7 +1,7 @@
# ChangeAtZ script - Change printing parameters at a given height
# This script is the successor of the TweakAtZ plugin for legacy Cura.
# It contains code from the TweakAtZ plugin V1.0-V4.x and from the ExampleScript by Jaime van Kessel, Ultimaker B.V.
-# It runs with the PostProcessingPlugin which is released under the terms of the AGPLv3 or higher.
+# It runs with the PostProcessingPlugin which is released under the terms of the LGPLv3 or higher.
# This script is licensed under the Creative Commons - Attribution - Share Alike (CC BY-SA) terms
# Authors of the ChangeAtZ plugin / script:
diff --git a/plugins/PostProcessingPlugin/scripts/ColorMix.py b/plugins/PostProcessingPlugin/scripts/ColorMix.py
index dacb40e9051..534c0208cf9 100644
--- a/plugins/PostProcessingPlugin/scripts/ColorMix.py
+++ b/plugins/PostProcessingPlugin/scripts/ColorMix.py
@@ -1,6 +1,6 @@
# ColorMix script - 2-1 extruder color mix and blending
# This script is specific for the Geeetech A10M dual extruder but should work with other Marlin printers.
-# It runs with the PostProcessingPlugin which is released under the terms of the AGPLv3 or higher.
+# It runs with the PostProcessingPlugin which is released under the terms of the LGPLv3 or higher.
# This script is licensed under the Creative Commons - Attribution - Share Alike (CC BY-SA) terms
#Authors of the 2-1 ColorMix plug-in / script:
diff --git a/plugins/PostProcessingPlugin/scripts/FilamentChange.py b/plugins/PostProcessingPlugin/scripts/FilamentChange.py
index d3c1e60192b..93941c09920 100644
--- a/plugins/PostProcessingPlugin/scripts/FilamentChange.py
+++ b/plugins/PostProcessingPlugin/scripts/FilamentChange.py
@@ -1,5 +1,5 @@
# Copyright (c) 2023 Ultimaker B.V.
-# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
+# The PostProcessingPlugin is released under the terms of the LGPLv3 or higher.
# Modification 06.09.2020
# add checkbox, now you can choose and use configuration from the firmware itself.
diff --git a/plugins/PostProcessingPlugin/scripts/RetractContinue.py b/plugins/PostProcessingPlugin/scripts/RetractContinue.py
index b5ea4d4edab..eaa15e0942f 100644
--- a/plugins/PostProcessingPlugin/scripts/RetractContinue.py
+++ b/plugins/PostProcessingPlugin/scripts/RetractContinue.py
@@ -1,5 +1,5 @@
# Copyright (c) 2023 UltiMaker B.V.
-# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
+# The PostProcessingPlugin is released under the terms of the LGPLv3 or higher.
from ..Script import Script
diff --git a/plugins/PostProcessingPlugin/scripts/SearchAndReplace.py b/plugins/PostProcessingPlugin/scripts/SearchAndReplace.py
index 40a56ace57f..7a12c229ccf 100644
--- a/plugins/PostProcessingPlugin/scripts/SearchAndReplace.py
+++ b/plugins/PostProcessingPlugin/scripts/SearchAndReplace.py
@@ -1,5 +1,5 @@
# Copyright (c) 2017 Ghostkeeper
-# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
+# The PostProcessingPlugin is released under the terms of the LGPLv3 or higher.
import re #To perform the search and replace.
diff --git a/plugins/PostProcessingPlugin/scripts/Stretch.py b/plugins/PostProcessingPlugin/scripts/Stretch.py
index 8d35f688223..ab964d160ad 100644
--- a/plugins/PostProcessingPlugin/scripts/Stretch.py
+++ b/plugins/PostProcessingPlugin/scripts/Stretch.py
@@ -1,4 +1,4 @@
-# This PostProcessingPlugin script is released under the terms of the AGPLv3 or higher.
+# This PostProcessingPlugin script is released under the terms of the LGPLv3 or higher.
"""
Copyright (c) 2017 Christophe Baribaud 2017
Python implementation of https://github.com/electrocbd/post_stretch
diff --git a/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py b/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py
index e93473c25f7..8c0c50d0b4a 100644
--- a/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py
+++ b/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py
@@ -1,6 +1,7 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
+import os
import os.path
from UM.Application import Application
@@ -143,38 +144,44 @@ def _onProgress(self, job, progress):
def _onFinished(self, job):
if self._stream:
- # Explicitly closing the stream flushes the write-buffer
+ error = job.getError()
try:
+ # Explicitly closing the stream flushes the write-buffer
self._stream.close()
- self._stream = None
- except:
- Logger.logException("w", "An exception occurred while trying to write to removable drive.")
- message = Message(catalog.i18nc("@info:status", "Could not save to removable drive {0}: {1}").format(self.getName(),str(job.getError())),
- title = catalog.i18nc("@info:title", "Error"),
- message_type = Message.MessageType.ERROR)
+ except Exception as e:
+ if not error:
+ # Only log new error if there was no previous one
+ error = e
+
+ self._stream = None
+ self._writing = False
+ self.writeFinished.emit(self)
+
+ if not error:
+ message = Message(
+ catalog.i18nc("@info:status", "Saved to Removable Drive {0} as {1}").format(self.getName(),
+ os.path.basename(
+ job.getFileName())),
+ title=catalog.i18nc("@info:title", "File Saved"),
+ message_type=Message.MessageType.POSITIVE)
+ message.addAction("eject", catalog.i18nc("@action:button", "Eject"), "eject",
+ catalog.i18nc("@action", "Eject removable device {0}").format(self.getName()))
+ message.actionTriggered.connect(self._onActionTriggered)
+ message.show()
+ self.writeSuccess.emit(self)
+ else:
+ try:
+ os.remove(job.getFileName())
+ except Exception as e:
+ Logger.logException("e", "Exception when trying to remove incomplete exported file %s",
+ str(job.getFileName()))
+ message = Message(catalog.i18nc("@info:status",
+ "Could not save to removable drive {0}: {1}").format(self.getName(),
+ str(job.getError())),
+ title=catalog.i18nc("@info:title", "Error"),
+ message_type=Message.MessageType.ERROR)
message.show()
self.writeError.emit(self)
- return
-
- self._writing = False
- self.writeFinished.emit(self)
- if job.getResult():
- message = Message(catalog.i18nc("@info:status", "Saved to Removable Drive {0} as {1}").format(self.getName(), os.path.basename(job.getFileName())),
- title = catalog.i18nc("@info:title", "File Saved"),
- message_type = Message.MessageType.POSITIVE)
- message.addAction("eject", catalog.i18nc("@action:button", "Eject"), "eject", catalog.i18nc("@action", "Eject removable device {0}").format(self.getName()))
- message.actionTriggered.connect(self._onActionTriggered)
- message.show()
- self.writeSuccess.emit(self)
- else:
- message = Message(catalog.i18nc("@info:status",
- "Could not save to removable drive {0}: {1}").format(self.getName(),
- str(job.getError())),
- title = catalog.i18nc("@info:title", "Error"),
- message_type = Message.MessageType.ERROR)
- message.show()
- self.writeError.emit(self)
- job.getStream().close()
def _onActionTriggered(self, message, action):
if action == "eject":
diff --git a/plugins/UM3NetworkPrinting/src/Messages/NewPrinterDetectedMessage.py b/plugins/UM3NetworkPrinting/src/Messages/NewPrinterDetectedMessage.py
index d85ade9dce1..76254547dab 100644
--- a/plugins/UM3NetworkPrinting/src/Messages/NewPrinterDetectedMessage.py
+++ b/plugins/UM3NetworkPrinting/src/Messages/NewPrinterDetectedMessage.py
@@ -37,24 +37,13 @@ def updateProgressText(self, output_device):
def finalize(self, new_devices_added, new_output_devices):
self.setProgress(None)
- num_devices_added = len(new_devices_added)
- max_disp_devices = 3
-
- if num_devices_added > max_disp_devices:
- num_hidden = num_devices_added - max_disp_devices
- device_name_list = ["