From 3be0e9f0797c14d15658ce32c3351d770c0ea884 Mon Sep 17 00:00:00 2001 From: DiyunZ Date: Mon, 24 Aug 2026 18:39:24 -0500 Subject: [PATCH 1/2] Fix download and cross-platform CMOD packaging Fixes #167 Fixes #168 --- .github/workflows/release.yml | 110 +++++--- BUILDING_LINUX.md | 18 +- BUILDING_MACOS.md | 22 +- BUILDING_WINDOWS.md | 12 +- CMOD/src/Utilities.cpp | 5 +- DOWNLOAD.md | 112 ++++++++ LASSIE/src/core/ReleaseAssetSelector.hpp | 99 +++++++ LASSIE/src/core/Updater.cpp | 48 +--- Make-Portable-for-Windows.ps1 | 332 ++++++++++++++++------- README.md | 10 +- packaging/Packaging.cmake | 118 ++++++-- packaging/linux/CMOD.desktop | 10 + packaging/linux/build-cmod-appimage.sh | 80 ++++++ packaging/macos/build-cmod-archive.sh | 87 ++++++ packaging/macos/fixup-dissco-app.sh | 96 +++++++ 15 files changed, 942 insertions(+), 217 deletions(-) create mode 100644 DOWNLOAD.md create mode 100644 LASSIE/src/core/ReleaseAssetSelector.hpp create mode 100644 packaging/linux/CMOD.desktop create mode 100755 packaging/linux/build-cmod-appimage.sh create mode 100755 packaging/macos/build-cmod-archive.sh create mode 100755 packaging/macos/fixup-dissco-app.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e6fbba1c..823b1341 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,7 +7,7 @@ on: workflow_dispatch: inputs: tag: - description: "Tag/version to release (e.g. v2.2.0). Optional; if empty, a draft release for the current SHA is produced." + description: "Tag/version for a draft release (e.g. v2.2.0). Leave empty to build workflow artifacts only." required: false permissions: @@ -15,7 +15,7 @@ permissions: jobs: macos: - name: macOS DMG + name: macOS arm64 packages runs-on: macos-14 steps: - uses: actions/checkout@v5 @@ -23,7 +23,7 @@ jobs: - name: Install dependencies run: | brew update - brew install cmake ninja libsndfile + brew install cmake ninja libsndfile dylibbundler - name: Install Qt 6.8 uses: jurplel/install-qt-action@v4 @@ -47,20 +47,19 @@ jobs: - name: Package (DMG) run: cmake --build build --target package - - name: Locate artifact - id: artifact - run: | - dmg=$(ls build/DISSCO-*-Darwin.dmg | head -n1) - echo "path=${dmg}" >> "$GITHUB_OUTPUT" - echo "name=$(basename "${dmg}")" >> "$GITHUB_OUTPUT" + - name: Package standalone CMOD + run: cmake --build build --target cmod-package - uses: actions/upload-artifact@v5 with: - name: ${{ steps.artifact.outputs.name }} - path: ${{ steps.artifact.outputs.path }} + name: macos-packages + if-no-files-found: error + path: | + build/DISSCO-*-Darwin.dmg + build/CMOD-*-Darwin-*.tar.gz linux: - name: Linux AppImage + name: Linux x86_64 packages # Pinned to 22.04 for older glibc: AppImages built here run on more # downstream distros than ones built on 24.04+. runs-on: ubuntu-22.04 @@ -107,21 +106,19 @@ jobs: QMAKE: ${{ env.QT_ROOT_DIR }}/bin/qmake run: cmake --build build --target appimage - - name: Locate artifact - id: artifact - run: | - img=$(ls build/DISSCO-*.AppImage | head -n1) - echo "path=${img}" >> "$GITHUB_OUTPUT" - echo "name=$(basename "${img}")" >> "$GITHUB_OUTPUT" + - name: Package standalone CMOD + run: cmake --build build --target cmod-package - uses: actions/upload-artifact@v5 with: - name: ${{ steps.artifact.outputs.name }} - path: ${{ steps.artifact.outputs.path }} + name: linux-packages + if-no-files-found: error + path: | + build/DISSCO-*-Linux-*.AppImage + build/CMOD-*-Linux-*.AppImage windows: - name: Windows installer - if: false + name: Windows x64 packages runs-on: windows-2022 defaults: run: @@ -135,6 +132,9 @@ jobs: with: arch: x64 + - name: Install NSIS + run: choco install nsis --yes --no-progress + - name: Install Qt 6.8 uses: jurplel/install-qt-action@v4 with: @@ -162,31 +162,77 @@ jobs: - name: Package (NSIS installer) run: cmake --build build --target package - - name: Locate artifact - id: artifact - run: | - $exe = Get-ChildItem build/DISSCO-*-Windows.exe | Select-Object -First 1 - "path=$($exe.FullName)" | Out-File -Append -Encoding utf8 $env:GITHUB_OUTPUT - "name=$($exe.Name)" | Out-File -Append -Encoding utf8 $env:GITHUB_OUTPUT + - name: Package standalone CMOD + run: cmake --build build --target cmod-package - uses: actions/upload-artifact@v5 with: - name: ${{ steps.artifact.outputs.name }} - path: ${{ steps.artifact.outputs.path }} + name: windows-packages + if-no-files-found: error + path: | + build/DISSCO-*-Windows.exe + build/CMOD-*-Windows-x64.zip release: name: Publish GitHub release needs: [macos, linux, windows] - if: startsWith(github.ref, 'refs/tags/v') + if: >- + startsWith(github.ref, 'refs/tags/v') || + (github.event_name == 'workflow_dispatch' && inputs.tag != '') runs-on: ubuntu-22.04 + env: + RELEASE_TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }} steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + - uses: actions/download-artifact@v5 with: path: dist merge-multiple: true + - name: Validate release tag and assets + run: | + major=$(sed -n 's/set(DISSCO_VER_MAJOR \([0-9][0-9]*\))/\1/p' DISSCOVersions.txt) + minor=$(sed -n 's/set(DISSCO_VER_MINOR \([0-9][0-9]*\))/\1/p' DISSCOVersions.txt) + patch=$(sed -n 's/set(DISSCO_VER_PATCH \([0-9][0-9]*\))/\1/p' DISSCOVersions.txt) + expected="v${major}.${minor}.${patch}" + if [[ "${RELEASE_TAG}" != "${expected}" ]]; then + echo "Release tag ${RELEASE_TAG} must match ${expected}." >&2 + exit 1 + fi + + if git show-ref --verify --quiet "refs/tags/${RELEASE_TAG}"; then + tag_commit=$(git rev-list -n 1 "refs/tags/${RELEASE_TAG}") + if [[ "${tag_commit}" != "${GITHUB_SHA}" ]]; then + echo "Existing tag ${RELEASE_TAG} must match the current commit ${GITHUB_SHA}." >&2 + exit 1 + fi + fi + + cd dist + for pattern in \ + 'DISSCO-*-Darwin.dmg' \ + 'DISSCO-*-Linux-*.AppImage' \ + 'DISSCO-*-Windows.exe' \ + 'CMOD-*-Darwin-*.tar.gz' \ + 'CMOD-*-Linux-*.AppImage' \ + 'CMOD-*-Windows-x64.zip'; do + compgen -G "${pattern}" > /dev/null || { + echo "Missing release asset: ${pattern}" >&2 + exit 1 + } + done + + - name: Generate SHA-256 checksums + working-directory: dist + run: sha256sum * > SHA256SUMS + - uses: softprops/action-gh-release@v2 with: + tag_name: ${{ env.RELEASE_TAG }} + target_commitish: ${{ github.sha }} files: dist/* - draft: true + draft: ${{ github.event_name == 'workflow_dispatch' }} generate_release_notes: true diff --git a/BUILDING_LINUX.md b/BUILDING_LINUX.md index af20d3cf..16f68e08 100644 --- a/BUILDING_LINUX.md +++ b/BUILDING_LINUX.md @@ -1,13 +1,15 @@ Building on Linux ================= +Looking for a released application instead of a source build? See [DOWNLOAD.md](DOWNLOAD.md). + Preliminary Requirements -------------------------- The following are *necessary* to compile anything: - git -- A C++17-supporting compiler (g++, clang), +- A C++20-supporting compiler (g++, clang), - A C compiler (gcc ...), and - cmake >= 3.25 @@ -48,11 +50,11 @@ Since we precompile headers for LASSIE and CMOD, we suggest `export CCACHE_SLOPP -Installing DISSCO ------------------ -Just `git clone` this repo; explicitly: +Getting the source +------------------ +Clone this repository: - git clone https://github.com/cmp-illinois/DISSCO-2.2.0.git + git clone https://github.com/cmp-illinois/DISSCO.git Building -------- @@ -89,3 +91,9 @@ From the project root: This produces `build/DISSCO--Linux-.AppImage` — a self-contained executable with Qt, libsndfile, and the `cmod` executable bundled in. Its desktop launcher runs `lassie`, and the AppImage works on most modern Linux distros without further installation. The first invocation downloads `linuxdeploy` and `linuxdeploy-plugin-qt` into `build/.linuxdeploy/`. The icon (`packaging/linux/LASSIE.png`) is a placeholder; replace it with real artwork before cutting a release. + +To build the standalone CMOD AppImage instead: + + cmake --build build --target cmod-package + +This produces `build/CMOD--Linux-.AppImage`. It contains CMOD and its audio-library dependencies, but not LASSIE, Qt, or LilyPond. diff --git a/BUILDING_MACOS.md b/BUILDING_MACOS.md index eb7935c0..46a47b82 100644 --- a/BUILDING_MACOS.md +++ b/BUILDING_MACOS.md @@ -1,13 +1,15 @@ Building on macOS ================= +Looking for a released application instead of a source build? See [DOWNLOAD.md](DOWNLOAD.md). + Preliminary Requirements -------------------------- The following are *necessary* to compile anything: - git -- A C++17-supporting compiler (g++, clang), +- A C++20-supporting compiler (g++, clang), - A C compiler (gcc ...), and - cmake >= 3.25 @@ -58,11 +60,11 @@ MacPorts will install it in `/opt/local/include/[your includes here]` and `/opt/ Since we precompile headers for LASSIE and CMOD, we suggest `export CCACHE_SLOPPINESS=pch_defines,time_macros`. Please review the `ccache` man page for more. -Installing DISSCO ------------------ -Just `git clone` this repo; explicitly: +Getting the source +------------------ +Clone this repository: - git clone https://github.com/cmp-illinois/DISSCO-2.2.0.git + git clone https://github.com/cmp-illinois/DISSCO.git Building -------- @@ -86,6 +88,10 @@ From `build`, you can clean `build` using `cmake --build . --target clean`. Alte Building a release DMG ---------------------- +The release packages also require `dylibbundler`: + + brew install dylibbundler + From the project root: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release \ @@ -98,3 +104,9 @@ This produces `build/DISSCO--Darwin.dmg`. The DMG contains `lassie.app` The build's icon (`packaging/macos/LASSIE.icns`) is a placeholder; regenerate it from updated artwork via `packaging/macos/make-icns.sh`. **Note on Gatekeeper**: without an Apple Developer ID signature + notarization, users opening the DMG will see a "cannot be verified" dialog and must right-click → Open. Code-signing and notarization can be added to the GitHub Actions release workflow once Apple Developer credentials are available. + +To build the standalone CMOD archive, run: + + cmake --build build --target cmod-package + +This produces `build/CMOD--Darwin-.tar.gz`. It contains CMOD and its audio-library dependencies, but not LASSIE, Qt, or LilyPond. diff --git a/BUILDING_WINDOWS.md b/BUILDING_WINDOWS.md index 13c58e2c..72327701 100644 --- a/BUILDING_WINDOWS.md +++ b/BUILDING_WINDOWS.md @@ -3,6 +3,8 @@ Building on Windows This guide explains how to build and run DISSCO on Windows using MSVC, CMake, Ninja, Qt, vcpkg, Git for Windows, and LilyPond. +Looking for a released application instead of a source build? See [DOWNLOAD.md](DOWNLOAD.md). + DISSCO contains three main parts: - **LASS**: sound synthesis library @@ -203,7 +205,7 @@ Choose where to put the source code. This guide assumes `C:\dev\DISSCO-2.2.0`: ```cmd cd /d C:\dev -git clone https://github.com/cmp-illinois/DISSCO-2.2.0.git +git clone https://github.com/cmp-illinois/DISSCO.git cd DISSCO-2.2.0 ``` @@ -387,3 +389,11 @@ For non-default paths or automation, run the PowerShell entry point directly: ``` Use `Get-Help .\Make-Portable-for-Windows.ps1 -Detailed` to view the accepted switches. + +To create only the standalone CMOD package from an existing Release build: + +```cmd +cmake --build build --target cmod-package +``` + +This produces `build\CMOD--Windows-x64.zip`. It includes CMOD, the Microsoft Visual C++ runtime, and CMOD's audio-library DLLs, but not LASSIE, Qt, or LilyPond. diff --git a/CMOD/src/Utilities.cpp b/CMOD/src/Utilities.cpp index 11d248c0..c97253d3 100644 --- a/CMOD/src/Utilities.cpp +++ b/CMOD/src/Utilities.cpp @@ -35,6 +35,7 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #include "Piece.h" #include "Patter.h" #include "ProbabilityEnvelope.h" // consider moving this into LASS.h +#include #include #include #include @@ -74,8 +75,8 @@ Utilities::Utilities(pugi::xml_node root, envelopeLibrary = new EnvelopeLibrary(); envelopeLibrary->loadLibraryNewFormat((char*)fileString.c_str()); - string deleteCommand = "rm " + fileString; - system(deleteCommand.c_str()); + std::error_code removalError; + std::filesystem::remove(fileString, removalError); // Construct Markov Model Library pugi::xml_node markovModelLibraryElement = GNES(envelopeLibraryElement); diff --git a/DOWNLOAD.md b/DOWNLOAD.md new file mode 100644 index 00000000..3273053b --- /dev/null +++ b/DOWNLOAD.md @@ -0,0 +1,112 @@ +# Download DISSCO and CMOD + +Use the assets on the [latest DISSCO release](https://github.com/cmp-illinois/DISSCO/releases/latest). GitHub Actions artifacts are temporary build outputs and are not supported downloads. + +DISSCO includes the LASSIE graphical editor and CMOD. Download a `DISSCO-*` asset for the complete application. Download a `CMOD-*` asset only when you want the command-line composition and synthesis tool without LASSIE. + +AppImage is a Linux format. The equivalent standalone CMOD downloads are a `.tar.gz` archive on macOS and a `.zip` archive on Windows. + +The automated release currently produces Windows x64, Linux x86_64, and macOS arm64 (Apple silicon) binaries. If there is no asset for your CPU architecture, use the source-build guides instead of running a mismatched binary. + +## Windows 10 or 11 (x64) + +### Complete DISSCO application + +1. Download `DISSCO--Windows.exe`. +2. Run the installer and follow its prompts. Windows may request administrator approval because the installer registers `.dissco` files for all users. +3. Start LASSIE from the Start menu or by opening a `.dissco` file. + +The installer includes LASSIE, CMOD, Qt, and CMOD's audio libraries. + +### Standalone CMOD + +1. Download `CMOD--Windows-x64.zip`. +2. Extract the entire ZIP. Do not run `cmod.exe` from inside the ZIP or copy it away from the included DLL files. +3. Open a terminal in the extracted folder and run: + + ```powershell + .\Run-CMOD.bat --help + .\Run-CMOD.bat C:\path\to\project.dissco + ``` + +The archive includes the Microsoft Visual C++ runtime and CMOD's audio libraries. It does not require administrator access. + +## macOS + +### Complete DISSCO application + +1. On an Apple silicon Mac, download `DISSCO--Darwin.dmg`. +2. Open the DMG and drag LASSIE to Applications. +3. Open LASSIE from Applications. + +The current release is not signed or notarized. If macOS blocks it, first try to open it, then go to **System Settings > Privacy & Security** and choose **Open Anyway** only after confirming that the file came from the official DISSCO release and its checksum matches. See [Apple's current safety guidance](https://support.apple.com/102445). + +### Standalone CMOD + +1. Download `CMOD--Darwin-.tar.gz`. The current automated release uses `arm64` for Apple silicon. +2. Extract the archive and run: + + ```bash + cd CMOD--Darwin- + ./bin/cmod --help + ./bin/cmod /path/to/project.dissco + ``` + +CMOD's audio libraries are included in the archive. The command-line binary is also unsigned, so the same Gatekeeper guidance may apply the first time it runs. + +## Linux + +Choose the asset whose architecture matches `uname -m`. The current automated release uses `x86_64`. + +### Complete DISSCO application + +Download `DISSCO--Linux-.AppImage`, then make it executable and run it: + +```bash +chmod +x DISSCO--Linux-.AppImage +./DISSCO--Linux-.AppImage +``` + +### Standalone CMOD + +Download `CMOD--Linux-.AppImage`, then make it executable and run it: + +```bash +chmod +x CMOD--Linux-.AppImage +./CMOD--Linux-.AppImage --help +./CMOD--Linux-.AppImage /path/to/project.dissco +``` + +See the [AppImage quickstart](https://docs.appimage.org/introduction/quickstart.html) for graphical instructions and troubleshooting. + +## Score and PDF output + +All release packages include the libraries needed for audio synthesis. They do not include LilyPond. Install [LilyPond](https://lilypond.org/download.html) separately and make its executable available on `PATH` when you need score or PDF output from either DISSCO or standalone CMOD. + +## Verify the download + +Download `SHA256SUMS` from the same release. Compare the checksum for the asset you downloaded before running it. + +On Linux: + +```bash +sha256sum DISSCO--Linux-.AppImage +``` + +On macOS: + +```bash +shasum -a 256 DISSCO--Darwin.dmg +``` + +On Windows PowerShell: + +```powershell +Get-FileHash .\DISSCO--Windows.exe -Algorithm SHA256 +``` + +The printed hash must match the corresponding line in `SHA256SUMS`. + +## Building from source + +These are download instructions for released binaries. Developers who need to build from source should use [BUILDING_LINUX.md](BUILDING_LINUX.md), [BUILDING_MACOS.md](BUILDING_MACOS.md), or [BUILDING_WINDOWS.md](BUILDING_WINDOWS.md). diff --git a/LASSIE/src/core/ReleaseAssetSelector.hpp b/LASSIE/src/core/ReleaseAssetSelector.hpp new file mode 100644 index 00000000..bdedc1d3 --- /dev/null +++ b/LASSIE/src/core/ReleaseAssetSelector.hpp @@ -0,0 +1,99 @@ +#ifndef RELEASE_ASSET_SELECTOR_HPP +#define RELEASE_ASSET_SELECTOR_HPP + +#include +#include +#include + +#include + +namespace DISSCO::ReleaseAssets { + +enum class Platform { + Unsupported, + Linux, + MacOS, + Windows +}; + +inline Platform currentPlatform() +{ +#if defined(Q_OS_MACOS) + return Platform::MacOS; +#elif defined(Q_OS_WIN) + return Platform::Windows; +#elif defined(Q_OS_LINUX) + return Platform::Linux; +#else + return Platform::Unsupported; +#endif +} + +inline QString platformSuffix(Platform platform) +{ + switch (platform) { + case Platform::Linux: + return QStringLiteral(".AppImage"); + case Platform::MacOS: + return QStringLiteral("-Darwin.dmg"); + case Platform::Windows: + return QStringLiteral("-Windows.exe"); + case Platform::Unsupported: + return QString(); + } + return QString(); +} + +inline QString packageArchitecture(QString architecture) +{ + architecture = architecture.trimmed().toLower(); + if (architecture == QStringLiteral("arm64")) { + return QStringLiteral("aarch64"); + } + if (architecture == QStringLiteral("amd64") || + architecture == QStringLiteral("x64")) { + return QStringLiteral("x86_64"); + } + return architecture; +} + +inline std::optional selectDisscoAsset( + const QJsonArray &assets, + Platform platform, + const QString &architecture) +{ + const QString suffix = platformSuffix(platform); + if (suffix.isEmpty()) { + return std::nullopt; + } + + const QString normalizedArchitecture = packageArchitecture(architecture); + std::optional firstMatch; + for (const QJsonValue &value : assets) { + const QJsonObject asset = value.toObject(); + const QString name = asset.value(QStringLiteral("name")).toString(); + if (!name.startsWith(QStringLiteral("DISSCO-")) || + !name.endsWith(suffix)) { + continue; + } + + if (!firstMatch.has_value()) { + firstMatch = asset; + } + if (platform == Platform::Linux && + !normalizedArchitecture.isEmpty() && + name.endsWith(QStringLiteral("-Linux-%1.AppImage") + .arg(normalizedArchitecture))) { + return asset; + } + } + + if (platform == Platform::Linux && !normalizedArchitecture.isEmpty()) { + return std::nullopt; + } + return firstMatch; +} + +} // namespace DISSCO::ReleaseAssets + +#endif // RELEASE_ASSET_SELECTOR_HPP diff --git a/LASSIE/src/core/Updater.cpp b/LASSIE/src/core/Updater.cpp index 7ecd992a..db421a58 100644 --- a/LASSIE/src/core/Updater.cpp +++ b/LASSIE/src/core/Updater.cpp @@ -1,4 +1,5 @@ #include "Updater.hpp" +#include "ReleaseAssetSelector.hpp" #include #include @@ -44,24 +45,6 @@ QVersionNumber parseTag(const QString &tag) { return QVersionNumber::fromString(trimmed); } -// Asset filename substring that identifies this platform's installer. -// Must stay in sync with packaging/Packaging.cmake's CPACK_PACKAGE_FILE_NAME -// and the workflow in .github/workflows/release.yml: -// macOS DISSCO--Darwin.dmg -// Linux DISSCO--Linux-.AppImage -// Windows DISSCO--Windows.exe -QString platformAssetMarker() { -#if defined(Q_OS_MACOS) - return QStringLiteral("-Darwin.dmg"); -#elif defined(Q_OS_WIN) - return QStringLiteral("-Windows.exe"); -#elif defined(Q_OS_LINUX) - return QStringLiteral(".AppImage"); -#else - return QString(); -#endif -} - } // namespace QVersionNumber Updater::currentVersion() { @@ -160,24 +143,17 @@ void Updater::onReleaseReply(QNetworkReply *reply) { return; } - // Find the asset matching this OS. - const QString marker = platformAssetMarker(); - if (!marker.isEmpty()) { - const QString archHint = QSysInfo::currentCpuArchitecture(); - const QJsonArray assets = obj.value("assets").toArray(); - for (const QJsonValue &v : assets) { - const QJsonObject a = v.toObject(); - const QString name = a.value("name").toString(); - if (!name.contains(marker)) continue; -#if defined(Q_OS_LINUX) - // Prefer an arch-matched build when multiple AppImages exist. - if (!name.contains(archHint) && assets.size() > 1) continue; -#endif - info.assetName = name; - info.assetUrl = QUrl(a.value("browser_download_url").toString()); - info.assetSize = static_cast(a.value("size").toDouble()); - break; - } + // Releases can also contain CMOD-only packages. Select only the DISSCO + // installer for this platform so the GUI updater never downloads CMOD. + const auto selectedAsset = DISSCO::ReleaseAssets::selectDisscoAsset( + obj.value("assets").toArray(), + DISSCO::ReleaseAssets::currentPlatform(), + QSysInfo::currentCpuArchitecture()); + if (selectedAsset.has_value()) { + const QJsonObject &asset = selectedAsset.value(); + info.assetName = asset.value("name").toString(); + info.assetUrl = QUrl(asset.value("browser_download_url").toString()); + info.assetSize = static_cast(asset.value("size").toDouble()); } if (!info.assetUrl.isValid()) { diff --git a/Make-Portable-for-Windows.ps1 b/Make-Portable-for-Windows.ps1 index 52aa39ef..585a99ca 100644 --- a/Make-Portable-for-Windows.ps1 +++ b/Make-Portable-for-Windows.ps1 @@ -5,8 +5,10 @@ param( [string]$LilyPondRoot, [string]$BuildDirectory = "build", [string]$PackageName = "DISSCO-Windows-x64", + [string]$OutputDirectory = "dist", [ValidateSet("Debug", "Release", "RelWithDebInfo", "MinSizeRel")] [string]$Configuration = "Release", + [switch]$CmodOnly, [switch]$SkipBuild, [switch]$SkipTests, [switch]$SkipSmokeTest, @@ -345,7 +347,8 @@ function Get-PeMachine { function Invoke-PortableSmokeTests { param( [Parameter(Mandatory = $true)][string]$PackageRoot, - [Parameter(Mandatory = $true)][string]$BundledLilyPondExe + [string]$BundledLilyPondExe, + [switch]$CmodOnly ) $savedEnvironment = @{} @@ -369,19 +372,21 @@ function Invoke-PortableSmokeTests { throw "CMOD smoke test failed with exit code $LASTEXITCODE." } - $lilyOutput = & $BundledLilyPondExe --version 2>&1 - if ($LASTEXITCODE -ne 0 -or ($lilyOutput -join "`n") -notmatch "GNU LilyPond") { - throw "Bundled LilyPond smoke test failed with exit code $LASTEXITCODE." - } + if (-not $CmodOnly) { + $lilyOutput = & $BundledLilyPondExe --version 2>&1 + if ($LASTEXITCODE -ne 0 -or ($lilyOutput -join "`n") -notmatch "GNU LilyPond") { + throw "Bundled LilyPond smoke test failed with exit code $LASTEXITCODE." + } - $guiProcess = Start-Process ` - -FilePath (Join-Path $PackageRoot "lassie.exe") ` - -WorkingDirectory $PackageRoot ` - -WindowStyle Hidden ` - -PassThru - Start-Sleep -Seconds 3 - if ($guiProcess.HasExited) { - throw "LASSIE exited during the portable smoke test (exit code $($guiProcess.ExitCode))." + $guiProcess = Start-Process ` + -FilePath (Join-Path $PackageRoot "lassie.exe") ` + -WorkingDirectory $PackageRoot ` + -WindowStyle Hidden ` + -PassThru + Start-Sleep -Seconds 3 + if ($guiProcess.HasExited) { + throw "LASSIE exited during the portable smoke test (exit code $($guiProcess.ExitCode))." + } } } finally { @@ -432,14 +437,31 @@ if (-not (Test-Path -LiteralPath $CTestExe -PathType Leaf)) { $CTestExe = $ctestCommand.Source } -$QtBin = Find-QtBin -RequestedQtBin $QtBin -CachePath $CachePath -$LilyPond = Find-LilyPond ` - -RequestedRoot $LilyPondRoot ` - -ProjectRoot $ProjectRoot ` - -PackageName $PackageName +$QtBin = $null +$LilyPond = $null +if (-not $CmodOnly) { + $QtBin = Find-QtBin -RequestedQtBin $QtBin -CachePath $CachePath + $LilyPond = Find-LilyPond ` + -RequestedRoot $LilyPondRoot ` + -ProjectRoot $ProjectRoot ` + -PackageName $PackageName +} $MsvcRuntimeDirectory = Find-MsvcRuntimeDirectory -CachePath $CachePath +$CachedSndFileDll = Get-CMakeCacheValue -CachePath $CachePath -Name "SNDFILE_DLL" +$SndFileRuntimeName = "sndfile.dll" +if (-not [string]::IsNullOrWhiteSpace($CachedSndFileDll) -and + -not $CachedSndFileDll.EndsWith("-NOTFOUND")) { + if (-not (Test-Path -LiteralPath $CachedSndFileDll -PathType Leaf)) { + throw "SNDFILE_DLL from CMakeCache.txt was not found: $CachedSndFileDll" + } + $CachedSndFileDll = (Resolve-Path -LiteralPath $CachedSndFileDll).Path + $SndFileRuntimeName = [System.IO.Path]::GetFileName($CachedSndFileDll) +} -$DistRoot = Join-Path $ProjectRoot "dist" +if (-not [System.IO.Path]::IsPathRooted($OutputDirectory)) { + $OutputDirectory = Join-Path $ProjectRoot $OutputDirectory +} +$DistRoot = [System.IO.Path]::GetFullPath($OutputDirectory) New-Item -ItemType Directory -Path $DistRoot -Force | Out-Null $DistRoot = (Resolve-Path -LiteralPath $DistRoot).Path $PackageRoot = Join-Path $DistRoot $PackageName @@ -448,6 +470,8 @@ $ChecksumPath = $ZipPath + ".sha256" $StageRoot = Join-Path $DistRoot (".$PackageName.staging-$PID") Assert-DirectChildPath -Parent $DistRoot -Child $PackageRoot +Assert-DirectChildPath -Parent $DistRoot -Child $ZipPath +Assert-DirectChildPath -Parent $DistRoot -Child $ChecksumPath Assert-DirectChildPath -Parent $DistRoot -Child $StageRoot if (Test-Path -LiteralPath $StageRoot) { Remove-Item -LiteralPath $StageRoot -Recurse -Force @@ -456,7 +480,12 @@ if (Test-Path -LiteralPath $StageRoot) { try { if (-not $SkipBuild) { Write-Host "[1/10] Building current $Configuration binaries..." -ForegroundColor Cyan - & $CMakeExe --build $BuildRoot --config $Configuration --parallel + if ($CmodOnly) { + & $CMakeExe --build $BuildRoot --config $Configuration --target CMOD --parallel + } + else { + & $CMakeExe --build $BuildRoot --config $Configuration --parallel + } if ($LASTEXITCODE -ne 0) { throw "$Configuration build failed." } @@ -477,39 +506,54 @@ try { } Write-Host "[3/10] Selecting fresh build outputs..." -ForegroundColor Cyan - $LassieExe = Find-BuiltExecutable ` - -BuildRoot $BuildRoot ` - -Component "LASSIE" ` - -Name "lassie.exe" ` - -Configuration $Configuration + $LassieExe = $null + if (-not $CmodOnly) { + $LassieExe = Find-BuiltExecutable ` + -BuildRoot $BuildRoot ` + -Component "LASSIE" ` + -Name "lassie.exe" ` + -Configuration $Configuration + } $CmodExe = Find-BuiltExecutable ` -BuildRoot $BuildRoot ` -Component "CMOD" ` -Name "cmod.exe" ` -Configuration $Configuration - Write-Host "LASSIE: $($LassieExe.FullName)" + if (-not $CmodOnly) { + Write-Host "LASSIE: $($LassieExe.FullName)" + } Write-Host "CMOD: $($CmodExe.FullName)" - Write-Host "Qt: $QtBin" - Write-Host "LilyPond: $($LilyPond.Root)" + if (-not $CmodOnly) { + Write-Host "Qt: $QtBin" + Write-Host "LilyPond: $($LilyPond.Root)" + } Write-Host "MSVC runtime: $MsvcRuntimeDirectory" Write-Host "[4/10] Creating an isolated staging directory..." -ForegroundColor Cyan New-Item -ItemType Directory -Path $StageRoot -Force | Out-Null - Copy-ExecutableAndAdjacentDlls -Executable $LassieExe -Destination $StageRoot + if (-not $CmodOnly) { + Copy-ExecutableAndAdjacentDlls -Executable $LassieExe -Destination $StageRoot + } Copy-ExecutableAndAdjacentDlls -Executable $CmodExe -Destination $StageRoot + if (-not [string]::IsNullOrWhiteSpace($CachedSndFileDll) -and + -not $CachedSndFileDll.EndsWith("-NOTFOUND")) { + Copy-Item -LiteralPath $CachedSndFileDll -Destination $StageRoot -Force + } Write-Host "[5/10] Deploying Qt and app-local MSVC runtime DLLs..." -ForegroundColor Cyan - $WinDeployQt = Join-Path $QtBin "windeployqt.exe" - & $WinDeployQt ` - --release ` - --force ` - --no-compiler-runtime ` - --no-translations ` - --no-quick-import ` - --dir $StageRoot ` - (Join-Path $StageRoot "lassie.exe") - if ($LASTEXITCODE -ne 0) { - throw "windeployqt failed." + if (-not $CmodOnly) { + $WinDeployQt = Join-Path $QtBin "windeployqt.exe" + & $WinDeployQt ` + --release ` + --force ` + --no-compiler-runtime ` + --no-translations ` + --no-quick-import ` + --dir $StageRoot ` + (Join-Path $StageRoot "lassie.exe") + if ($LASTEXITCODE -ne 0) { + throw "windeployqt failed." + } } Get-ChildItem -LiteralPath $MsvcRuntimeDirectory -File -Filter "*.dll" | @@ -520,35 +564,53 @@ try { Remove-Item -Force Write-Host "[6/10] Bundling LilyPond and documentation..." -ForegroundColor Cyan - $BundledLilyPondRoot = Join-Path $StageRoot "tools\lilypond" - New-Item -ItemType Directory -Path $BundledLilyPondRoot -Force | Out-Null - Get-ChildItem -LiteralPath $LilyPond.Root -Force | - ForEach-Object { - Copy-Item -LiteralPath $_.FullName -Destination $BundledLilyPondRoot -Recurse -Force - } - - $BundledLilyBin = if ([string]::IsNullOrWhiteSpace($LilyPond.RelativeBin)) { - $BundledLilyPondRoot - } - else { - Join-Path $BundledLilyPondRoot $LilyPond.RelativeBin - } - $BundledLilyPondExe = Join-Path $BundledLilyBin "lilypond.exe" + $BundledLilyPondExe = $null + $BundledLilyBin = $null + if (-not $CmodOnly) { + $BundledLilyPondRoot = Join-Path $StageRoot "tools\lilypond" + New-Item -ItemType Directory -Path $BundledLilyPondRoot -Force | Out-Null + Get-ChildItem -LiteralPath $LilyPond.Root -Force | + ForEach-Object { + Copy-Item -LiteralPath $_.FullName -Destination $BundledLilyPondRoot -Recurse -Force + } - if (Test-Path -LiteralPath (Join-Path $ProjectRoot "Documents") -PathType Container) { - Copy-Item ` - -LiteralPath (Join-Path $ProjectRoot "Documents") ` - -Destination (Join-Path $StageRoot "Documentation") ` - -Recurse ` - -Force + $BundledLilyBin = if ([string]::IsNullOrWhiteSpace($LilyPond.RelativeBin)) { + $BundledLilyPondRoot + } + else { + Join-Path $BundledLilyPondRoot $LilyPond.RelativeBin + } + $BundledLilyPondExe = Join-Path $BundledLilyBin "lilypond.exe" + + if (Test-Path -LiteralPath (Join-Path $ProjectRoot "Documents") -PathType Container) { + Copy-Item ` + -LiteralPath (Join-Path $ProjectRoot "Documents") ` + -Destination (Join-Path $StageRoot "Documentation") ` + -Recurse ` + -Force + } + New-Item -ItemType Directory -Path (Join-Path $StageRoot "SoundFiles") -Force | Out-Null + New-Item -ItemType Directory -Path (Join-Path $StageRoot "ScoreFiles") -Force | Out-Null } Copy-Item -LiteralPath (Join-Path $ProjectRoot "LICENSE") -Destination $StageRoot -Force - New-Item -ItemType Directory -Path (Join-Path $StageRoot "SoundFiles") -Force | Out-Null - New-Item -ItemType Directory -Path (Join-Path $StageRoot "ScoreFiles") -Force | Out-Null Write-Host "[7/10] Writing portable launchers and package information..." -ForegroundColor Cyan - $launcherLilyBin = $BundledLilyBin.Substring($StageRoot.Length).TrimStart('\', '/') - $launcher = @" + if ($CmodOnly) { + $launcher = @" +@echo off +setlocal +cd /d "%~dp0" +"%~dp0cmod.exe" %* +endlocal +"@ + Set-Content ` + -LiteralPath (Join-Path $StageRoot "Run-CMOD.bat") ` + -Value $launcher ` + -Encoding ASCII + } + else { + $launcherLilyBin = $BundledLilyBin.Substring($StageRoot.Length).TrimStart('\', '/') + $launcher = @" @echo off setlocal cd /d "%~dp0" @@ -556,10 +618,11 @@ set "PATH=%~dp0$launcherLilyBin;%PATH%" start "" "%~dp0lassie.exe" %* endlocal "@ - Set-Content ` - -LiteralPath (Join-Path $StageRoot "Run-DISSCO.bat") ` - -Value $launcher ` - -Encoding ASCII + Set-Content ` + -LiteralPath (Join-Path $StageRoot "Run-DISSCO.bat") ` + -Value $launcher ` + -Encoding ASCII + } $versionFile = Join-Path $ProjectRoot "DISSCOVersions.txt" $versionText = Get-Content -LiteralPath $versionFile -Raw @@ -578,13 +641,35 @@ endlocal "" } - # Keep this script ASCII-compatible for Windows PowerShell 5, which reads - # UTF-8-without-BOM source files using the active Windows code page. - $chineseInstructions = [Text.Encoding]::UTF8.GetString( - [Convert]::FromBase64String( - "5Lit5paH6K+05piOCiAg6Kej5Y6L5pW05LiqIFpJUCDlkI7vvIznm7TmjqXlj4zlh7sgbGFzc2llLmV4ZeOAggogIOS4jemcgOimgeWuieijhSBRdOOAgVZpc3VhbCBDKysg6L+Q6KGM5bqT5oiWIExpbHlQb25k77yM5Lmf5LiN6ZyA6KaB566h55CG5ZGY5p2D6ZmQ44CCCiAg6K+35Yu/5Y+q5aSN5Yi25Y2V5LiqIEVYRe+8m3Rvb2xz44CBcGxhdGZvcm1zIOWSjCBETEwg5paH5Lu26YO95piv56iL5bqP55qE5LiA6YOo5YiG44CC")) + if ($CmodOnly) { + $readme = @" +CMOD $version Portable for Windows x64 +====================================== + +RUN + Run-CMOD.bat --help + Run-CMOD.bat C:\path\to\project.dissco - $readme = @" +REQUIREMENTS + - 64-bit Windows 10 or Windows 11 + - Extract the entire ZIP before running it + - Keep every file and folder together + +The Microsoft Visual C++ runtime and CMOD's audio libraries are included. +LilyPond is not included; install it separately and add it to PATH for score +or PDF output. Audio synthesis does not require LilyPond. + +Build: $commit$dirtySuffix +"@ + } + else { + # Keep this script ASCII-compatible for Windows PowerShell 5, which reads + # UTF-8-without-BOM source files using the active Windows code page. + $chineseInstructions = [Text.Encoding]::UTF8.GetString( + [Convert]::FromBase64String( + "5Lit5paH6K+05piOCiAg6Kej5Y6L5pW05LiqIFpJUCDlkI7vvIznm7TmjqXlj4zlh7sgbGFzc2llLmV4ZeOAggogIOS4jemcgOimgeWuieijhSBRdOOAgVZpc3VhbCBDKysg6L+Q6KGM5bqT5oiWIExpbHlQb25k77yM5Lmf5LiN6ZyA6KaB566h55CG5ZGY5p2D6ZmQ44CCCiAg6K+35Yu/5Y+q5aSN5Yi25Y2V5LiqIEVYRe+8m3Rvb2xz44CBcGxhdGZvcm1zIOWSjCBETEwg5paH5Lu26YO95piv56iL5bqP55qE5LiA6YOo5YiG44CC")) + + $readme = @" DISSCO $version Portable for Windows x64 ======================================== @@ -606,55 +691,95 @@ $chineseInstructions Build: $commit$dirtySuffix "@ + } Set-Content ` -LiteralPath (Join-Path $StageRoot "README.txt") ` -Value $readme ` -Encoding UTF8 Write-Host "[8/10] Validating package contents and architecture..." -ForegroundColor Cyan - $requiredPaths = @( - "lassie.exe", - "cmod.exe", - "Qt6Core.dll", - "Qt6Gui.dll", - "Qt6Widgets.dll", - "platforms\qwindows.dll", - "msvcp140.dll", - "msvcp140_1.dll", - "vcruntime140.dll", - "vcruntime140_1.dll", - "Run-DISSCO.bat", - "README.txt", - "LICENSE" - ) + $requiredPaths = if ($CmodOnly) { + @( + "cmod.exe", + $SndFileRuntimeName, + "msvcp140.dll", + "msvcp140_1.dll", + "vcruntime140.dll", + "vcruntime140_1.dll", + "Run-CMOD.bat", + "README.txt", + "LICENSE" + ) + } + else { + @( + "lassie.exe", + "cmod.exe", + "Qt6Core.dll", + "Qt6Gui.dll", + "Qt6Widgets.dll", + "platforms\qwindows.dll", + "msvcp140.dll", + "msvcp140_1.dll", + "vcruntime140.dll", + "vcruntime140_1.dll", + "Run-DISSCO.bat", + "README.txt", + "LICENSE" + ) + } $missing = @($requiredPaths | Where-Object { -not (Test-Path -LiteralPath (Join-Path $StageRoot $_) -PathType Leaf) }) - if (-not (Test-Path -LiteralPath $BundledLilyPondExe -PathType Leaf)) { + if (-not $CmodOnly -and + -not (Test-Path -LiteralPath $BundledLilyPondExe -PathType Leaf)) { $missing += $BundledLilyPondExe } if ($missing.Count -gt 0) { throw "Portable package validation failed. Missing: $($missing -join ', ')" } - foreach ($peFile in @( - (Join-Path $StageRoot "lassie.exe"), - (Join-Path $StageRoot "cmod.exe"), - (Join-Path $StageRoot "Qt6Core.dll") - )) { + $peFiles = if ($CmodOnly) { + @((Join-Path $StageRoot "cmod.exe")) + + @(Get-ChildItem -LiteralPath $StageRoot -File -Filter "*.dll" | + ForEach-Object { $_.FullName }) + } + else { + @( + (Join-Path $StageRoot "lassie.exe"), + (Join-Path $StageRoot "cmod.exe"), + (Join-Path $StageRoot "Qt6Core.dll") + ) + } + foreach ($peFile in $peFiles) { if ((Get-PeMachine -Path $peFile) -ne 0x8664) { throw "The portable package contains a non-x64 binary: $peFile" } } - foreach ($sourceAndPackaged in @( - @($LassieExe.FullName, (Join-Path $StageRoot "lassie.exe")), - @($CmodExe.FullName, (Join-Path $StageRoot "cmod.exe")) - )) { - $sourceHash = (Get-FileHash -LiteralPath $sourceAndPackaged[0] -Algorithm SHA256).Hash - $packagedHash = (Get-FileHash -LiteralPath $sourceAndPackaged[1] -Algorithm SHA256).Hash + $packagedExecutables = if ($CmodOnly) { + @([pscustomobject]@{ + Source = $CmodExe.FullName + Packaged = (Join-Path $StageRoot "cmod.exe") + }) + } + else { + @( + [pscustomobject]@{ + Source = $LassieExe.FullName + Packaged = (Join-Path $StageRoot "lassie.exe") + }, + [pscustomobject]@{ + Source = $CmodExe.FullName + Packaged = (Join-Path $StageRoot "cmod.exe") + } + ) + } + foreach ($sourceAndPackaged in $packagedExecutables) { + $sourceHash = (Get-FileHash -LiteralPath $sourceAndPackaged.Source -Algorithm SHA256).Hash + $packagedHash = (Get-FileHash -LiteralPath $sourceAndPackaged.Packaged -Algorithm SHA256).Hash if ($sourceHash -ne $packagedHash) { - throw "Packaged executable differs from the current build: $($sourceAndPackaged[1])" + throw "Packaged executable differs from the current build: $($sourceAndPackaged.Packaged)" } } @@ -662,7 +787,8 @@ Build: $commit$dirtySuffix Write-Host "[9/10] Testing without developer Qt/MSVC/LilyPond paths..." -ForegroundColor Cyan Invoke-PortableSmokeTests ` -PackageRoot $StageRoot ` - -BundledLilyPondExe $BundledLilyPondExe + -BundledLilyPondExe $BundledLilyPondExe ` + -CmodOnly:$CmodOnly } else { Write-Host "[9/10] Portable smoke tests skipped by request." -ForegroundColor DarkGray diff --git a/README.md b/README.md index 0dd424ea..ca2204b3 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ -# DISSCO-2.2.0 -Major update to DISSCO focusing on cross-platform compatibility and improved usability. +# DISSCO -Please refer to the `BUILDING.md` file relevant to the operating system you wish to install on. +DISSCO combines the LASSIE graphical editor with the CMOD composition and synthesis engine. -# Contributing +- [Download DISSCO or CMOD](DOWNLOAD.md) for Linux, macOS, and Windows +- Build from source on [Linux](BUILDING_LINUX.md), [macOS](BUILDING_MACOS.md), or [Windows](BUILDING_WINDOWS.md) + +## Contributing Thank you for your interest in contributing to the dissco project. diff --git a/packaging/Packaging.cmake b/packaging/Packaging.cmake index b3d2ffe3..37ce24c5 100644 --- a/packaging/Packaging.cmake +++ b/packaging/Packaging.cmake @@ -18,6 +18,7 @@ # Trigger: # cmake --build build --target package # macOS DMG / Windows .exe # cmake --build build --target appimage # Linux AppImage +# cmake --build build --target cmod-package # CMOD-only platform package set(CPACK_PACKAGE_NAME "DISSCO") set(CPACK_PACKAGE_VENDOR "DISSCO Project") @@ -31,40 +32,45 @@ set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/LICENSE") set(CPACK_PACKAGE_FILE_NAME "DISSCO-${DISSCO_VERSION}-${CMAKE_SYSTEM_NAME}") if(APPLE) - # macdeployqt lives in Qt6's bin directory; ask the imported target where - # that is so we don't depend on PATH. + # Locate every tool used by the install-time app fixup. Missing optional + # packaging tools are reported when `package` runs, not during a normal + # developer build. get_target_property(_qmake_executable Qt6::qmake IMPORTED_LOCATION) get_filename_component(_qt_bin_dir "${_qmake_executable}" DIRECTORY) + get_filename_component(_qt_root_dir "${_qt_bin_dir}" DIRECTORY) set(MACDEPLOYQT_EXECUTABLE "${_qt_bin_dir}/macdeployqt") + find_program(DYLIBBUNDLER_EXECUTABLE NAMES dylibbundler) + find_program(OTOOL_EXECUTABLE NAMES otool) + find_program(BASH_EXECUTABLE NAMES bash REQUIRED) - # Bundle Qt frameworks into the installed lassie.app. Runs at `cmake - # --install` time (and therefore during CPack), after lassie and cmod - # have been copied in. - # - # macdeployqt prints alarming-looking "ERROR: Cannot resolve rpath" lines - # for weak-linked Qt modules (QtPdf, QtSvg, QtVirtualKeyboard*) that - # aren't installed via brew's qt@6 and that LASSIE doesn't actually use. - # These are non-fatal: macdeployqt still exits 0 and the bundle is - # valid. We capture output and drop those known-benign lines so real - # problems remain visible. + # Both app executables link libsndfile, while macdeployqt only deploys Qt + # dependencies. Run the dedicated fixup after lassie and cmod are installed + # so it can bundle third-party dylibs, deploy Qt, and smoke-test embedded + # CMOD from the staged app before CPack creates the DMG. install(CODE " - message(STATUS \"Running macdeployqt on \${CMAKE_INSTALL_PREFIX}/lassie.app\") + message(STATUS \"Fixing up \${CMAKE_INSTALL_PREFIX}/lassie.app\") execute_process( - COMMAND \"${MACDEPLOYQT_EXECUTABLE}\" - \"\${CMAKE_INSTALL_PREFIX}/lassie.app\" - -always-overwrite - RESULT_VARIABLE _mdq_result - OUTPUT_VARIABLE _mdq_out - ERROR_VARIABLE _mdq_err + COMMAND \"${CMAKE_COMMAND}\" -E env + \"APP_BUNDLE=\${CMAKE_INSTALL_PREFIX}/lassie.app\" + \"DYLIBBUNDLER=${DYLIBBUNDLER_EXECUTABLE}\" + \"MACDEPLOYQT=${MACDEPLOYQT_EXECUTABLE}\" + \"OTOOL=${OTOOL_EXECUTABLE}\" + \"QT_ROOT=${_qt_root_dir}\" + \"${BASH_EXECUTABLE}\" + \"${CMAKE_SOURCE_DIR}/packaging/macos/fixup-dissco-app.sh\" + RESULT_VARIABLE _fixup_result + OUTPUT_VARIABLE _fixup_out + ERROR_VARIABLE _fixup_err ) - set(_mdq_combined \"\${_mdq_out}\${_mdq_err}\") - string(REGEX REPLACE \"(ERROR: Cannot resolve rpath \\\"@rpath/Qt(Pdf|Svg|VirtualKeyboard[A-Za-z]*)\\\\.framework[^\\n]*\\n)\" \"\" _mdq_filtered \"\${_mdq_combined}\") - string(REGEX REPLACE \"(ERROR: using QList[^\\n]*\\n)\" \"\" _mdq_filtered \"\${_mdq_filtered}\") - if(_mdq_filtered) - message(\"\${_mdq_filtered}\") + if(_fixup_out) + message(\"\${_fixup_out}\") + endif() + if(_fixup_err) + message(\"\${_fixup_err}\") endif() - if(NOT _mdq_result EQUAL 0) - message(FATAL_ERROR \"macdeployqt failed with exit code \${_mdq_result}\") + if(NOT _fixup_result EQUAL 0) + message(FATAL_ERROR + \"macOS app fixup failed with exit code \${_fixup_result}\") endif() " COMPONENT Runtime) @@ -115,9 +121,9 @@ if(WIN32) set(CPACK_GENERATOR "NSIS") set(CPACK_NSIS_PACKAGE_NAME "DISSCO ${DISSCO_VERSION}") set(CPACK_NSIS_DISPLAY_NAME "DISSCO ${DISSCO_VERSION}") - set(CPACK_NSIS_HELP_LINK "https://github.com/cmp-illinois/DISSCO-2.2.0") - set(CPACK_NSIS_URL_INFO_ABOUT "https://github.com/cmp-illinois/DISSCO-2.2.0") - set(CPACK_NSIS_CONTACT "https://github.com/cmp-illinois/DISSCO-2.2.0/issues") + set(CPACK_NSIS_HELP_LINK "https://github.com/cmp-illinois/DISSCO") + set(CPACK_NSIS_URL_INFO_ABOUT "https://github.com/cmp-illinois/DISSCO") + set(CPACK_NSIS_CONTACT "https://github.com/cmp-illinois/DISSCO/issues") set(CPACK_NSIS_MODIFY_PATH OFF) set(CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL ON) set(CPACK_PACKAGE_INSTALL_DIRECTORY "DISSCO ${DISSCO_VERSION}") @@ -192,4 +198,58 @@ if(UNIX AND NOT APPLE) ) endif() +# CMOD is also distributed as a standalone command-line package. These targets +# stage directly from the CMOD executable and leave the combined DISSCO Runtime +# component untouched. +if(UNIX AND NOT APPLE) + add_custom_target(cmod-package + COMMAND ${CMAKE_COMMAND} -E env + "DISSCO_VERSION=${DISSCO_VERSION}" + "APPDIR=${CMAKE_BINARY_DIR}/CmodAppDir" + "OUTPUT_DIR=${CMAKE_BINARY_DIR}" + "SOURCE_DIR=${CMAKE_SOURCE_DIR}" + "CMOD_BINARY=$" + bash "${CMAKE_SOURCE_DIR}/packaging/linux/build-cmod-appimage.sh" + DEPENDS CMOD + WORKING_DIRECTORY "${CMAKE_BINARY_DIR}" + COMMENT "Building standalone CMOD AppImage" + VERBATIM + ) +elseif(APPLE) + add_custom_target(cmod-package + COMMAND ${CMAKE_COMMAND} -E env + "DISSCO_VERSION=${DISSCO_VERSION}" + "OUTPUT_DIR=${CMAKE_BINARY_DIR}" + "SOURCE_DIR=${CMAKE_SOURCE_DIR}" + "CMOD_BINARY=$" + bash "${CMAKE_SOURCE_DIR}/packaging/macos/build-cmod-archive.sh" + DEPENDS CMOD + WORKING_DIRECTORY "${CMAKE_BINARY_DIR}" + COMMENT "Building standalone CMOD macOS archive" + VERBATIM + ) +elseif(WIN32) + find_program(POWERSHELL_EXECUTABLE + NAMES pwsh powershell + REQUIRED + ) + add_custom_target(cmod-package + COMMAND "${POWERSHELL_EXECUTABLE}" + -NoProfile + -ExecutionPolicy Bypass + -File "${CMAKE_SOURCE_DIR}/Make-Portable-for-Windows.ps1" + -ProjectRoot "${CMAKE_SOURCE_DIR}" + -BuildDirectory "${CMAKE_BINARY_DIR}" + -OutputDirectory "${CMAKE_BINARY_DIR}" + -PackageName "CMOD-${DISSCO_VERSION}-Windows-x64" + -CmodOnly + -SkipBuild + -SkipTests + DEPENDS CMOD + WORKING_DIRECTORY "${CMAKE_BINARY_DIR}" + COMMENT "Building standalone CMOD Windows archive" + VERBATIM + ) +endif() + include(CPack) diff --git a/packaging/linux/CMOD.desktop b/packaging/linux/CMOD.desktop new file mode 100644 index 00000000..c8c27fbd --- /dev/null +++ b/packaging/linux/CMOD.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Type=Application +Name=CMOD +Comment=Render a DISSCO composition from the command line +Exec=cmod %f +Icon=CMOD +Terminal=true +Categories=AudioVideo;Audio; +MimeType=application/x-dissco; +Keywords=composition;music;sound;synthesis;dissco;cmod; diff --git a/packaging/linux/build-cmod-appimage.sh b/packaging/linux/build-cmod-appimage.sh new file mode 100755 index 00000000..143d7f92 --- /dev/null +++ b/packaging/linux/build-cmod-appimage.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +# Build a CMOD-only AppImage from the compiled CMOD executable. + +set -euo pipefail + +: "${APPDIR:?APPDIR not set}" +: "${OUTPUT_DIR:?OUTPUT_DIR not set}" +: "${SOURCE_DIR:?SOURCE_DIR not set}" +: "${DISSCO_VERSION:?DISSCO_VERSION not set}" +: "${CMOD_BINARY:?CMOD_BINARY not set}" + +resolved_appdir="$(realpath -m "${APPDIR}")" +resolved_output="$(realpath -m "${OUTPUT_DIR}")" +resolved_source="$(realpath -m "${SOURCE_DIR}")" +resolved_appdir_parent="$(dirname -- "${resolved_appdir}")" +if [[ "${resolved_appdir}" == "/" || + "${resolved_appdir}" == "${resolved_output}" || + "${resolved_appdir}" == "${resolved_source}" || + "${resolved_appdir_parent}" != "${resolved_output}" ]]; then + echo "Refusing unsafe CMOD AppDir path: ${resolved_appdir}" >&2 + exit 1 +fi + +rm -rf -- "${resolved_appdir}" +mkdir -p "${resolved_appdir}/usr/bin" +mkdir -p "${resolved_appdir}/usr/share/applications" +mkdir -p "${resolved_appdir}/usr/share/icons/hicolor/256x256/apps" +mkdir -p "${resolved_output}" + +install -m 755 "${CMOD_BINARY}" "${resolved_appdir}/usr/bin/cmod" +install -m 644 "${SOURCE_DIR}/packaging/linux/CMOD.desktop" \ + "${resolved_appdir}/usr/share/applications/CMOD.desktop" +install -m 644 "${SOURCE_DIR}/packaging/linux/LASSIE.png" \ + "${resolved_appdir}/usr/share/icons/hicolor/256x256/apps/CMOD.png" + +arch="$(uname -m)" +tools_dir="${resolved_output}/.linuxdeploy" +mkdir -p "${tools_dir}" + +fetch() { + local url="$1" destination="$2" + if [[ ! -x "${destination}" ]]; then + echo "Downloading ${url}" + curl -fSL -o "${destination}" "${url}" + chmod +x "${destination}" + fi +} + +if [[ -n "${LINUXDEPLOY:-}" ]]; then + linuxdeploy="${LINUXDEPLOY}" +else + linuxdeploy="${tools_dir}/linuxdeploy-${arch}.AppImage" + fetch \ + "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-${arch}.AppImage" \ + "${linuxdeploy}" +fi + +output_name="CMOD-${DISSCO_VERSION}-Linux-${arch}.AppImage" +output_path="${resolved_output}/${output_name}" +rm -f -- "${output_path}" + +# GitHub-hosted runners do not expose FUSE to nested AppImages. +export APPIMAGE_EXTRACT_AND_RUN=1 + +cd "${resolved_output}" +OUTPUT="${output_name}" \ +"${linuxdeploy}" \ + --appdir "${resolved_appdir}" \ + --executable "${resolved_appdir}/usr/bin/cmod" \ + --desktop-file "${resolved_appdir}/usr/share/applications/CMOD.desktop" \ + --icon-file "${resolved_appdir}/usr/share/icons/hicolor/256x256/apps/CMOD.png" \ + --output appimage + +if [[ ! -x "${output_path}" ]]; then + echo "linuxdeploy did not create ${output_path}" >&2 + exit 1 +fi + +"${output_path}" --help | grep -Fq "Usage:" +echo "Built ${output_path}" diff --git a/packaging/macos/build-cmod-archive.sh b/packaging/macos/build-cmod-archive.sh new file mode 100755 index 00000000..8f5d9de5 --- /dev/null +++ b/packaging/macos/build-cmod-archive.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# Build a relocatable CMOD command-line archive for macOS. + +set -euo pipefail + +: "${OUTPUT_DIR:?OUTPUT_DIR not set}" +: "${SOURCE_DIR:?SOURCE_DIR not set}" +: "${DISSCO_VERSION:?DISSCO_VERSION not set}" +: "${CMOD_BINARY:?CMOD_BINARY not set}" + +mkdir -p "${OUTPUT_DIR}" + +if [[ -n "${DYLIBBUNDLER:-}" ]]; then + dylibbundler="${DYLIBBUNDLER}" +else + dylibbundler="$(command -v dylibbundler || true)" +fi +if [[ -z "${dylibbundler}" || ! -x "${dylibbundler}" ]]; then + echo "dylibbundler is required to make the CMOD macOS archive" >&2 + exit 1 +fi + +if [[ -n "${OTOOL:-}" ]]; then + otool="${OTOOL}" +else + otool="$(command -v otool || true)" +fi +if [[ -z "${otool}" || ! -x "${otool}" ]]; then + echo "otool is required to validate the CMOD macOS archive" >&2 + exit 1 +fi + +arch="$(uname -m)" +package_name="CMOD-${DISSCO_VERSION}-Darwin-${arch}" +archive_path="${OUTPUT_DIR}/${package_name}.tar.gz" +staging_root="$(mktemp -d "${OUTPUT_DIR}/.cmod-macos.XXXXXX")" +trap 'rm -rf "${staging_root}"' EXIT +package_root="${staging_root}/${package_name}" + +mkdir -p "${package_root}/bin" "${package_root}/lib" +install -m 755 "${CMOD_BINARY}" "${package_root}/bin/cmod" +install -m 644 "${SOURCE_DIR}/LICENSE" "${package_root}/LICENSE" + +"${dylibbundler}" \ + -od \ + -b \ + -x "${package_root}/bin/cmod" \ + -d "${package_root}/lib/" \ + -p "@executable_path/../lib/" + +cat > "${package_root}/README.txt" <&2 + echo "${dependency_report}" >&2 + exit 1 +fi + +clean_path="/usr/bin:/bin" +env -i HOME="${HOME:-/tmp}" PATH="${clean_path}" \ + "${package_root}/bin/cmod" --help | grep -Fq "Usage:" + +rm -f -- "${archive_path}" +tar -czf "${archive_path}" -C "${staging_root}" "${package_name}" +echo "Built ${archive_path}" diff --git a/packaging/macos/fixup-dissco-app.sh b/packaging/macos/fixup-dissco-app.sh new file mode 100755 index 00000000..bba9ca0a --- /dev/null +++ b/packaging/macos/fixup-dissco-app.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +# Make the staged DISSCO app self-contained and verify its embedded CMOD. + +set -euo pipefail + +: "${APP_BUNDLE:?APP_BUNDLE not set}" +: "${DYLIBBUNDLER:?DYLIBBUNDLER not set}" +: "${MACDEPLOYQT:?MACDEPLOYQT not set}" +: "${OTOOL:?OTOOL not set}" +: "${QT_ROOT:?QT_ROOT not set}" + +if [[ ! -d "${APP_BUNDLE}" ]]; then + echo "DISSCO app bundle was not found: ${APP_BUNDLE}" >&2 + exit 1 +fi +for tool in "${DYLIBBUNDLER}" "${MACDEPLOYQT}" "${OTOOL}"; do + if [[ ! -x "${tool}" ]]; then + echo "Required macOS packaging tool was not found: ${tool}" >&2 + exit 1 + fi +done + +lassie="${APP_BUNDLE}/Contents/MacOS/lassie" +cmod="${APP_BUNDLE}/Contents/MacOS/cmod" +frameworks="${APP_BUNDLE}/Contents/Frameworks" +for executable in "${lassie}" "${cmod}"; do + if [[ ! -x "${executable}" ]]; then + echo "DISSCO app executable was not found: ${executable}" >&2 + exit 1 + fi +done +mkdir -p "${frameworks}" + +# LASSIE and CMOD both link libsndfile. Keep Qt for macdeployqt, and use +# dylibbundler only for the remaining third-party dylibs and their closure. +"${DYLIBBUNDLER}" \ + -od \ + -b \ + -x "${lassie}" \ + -x "${cmod}" \ + -d "${frameworks}/" \ + -p "@executable_path/../Frameworks/" \ + -i "${QT_ROOT}" \ + -i "/System/Library" + +set +e +macdeploy_output="$( + "${MACDEPLOYQT}" \ + "${APP_BUNDLE}" \ + "-executable=${cmod}" \ + -always-overwrite 2>&1 +)" +macdeploy_result=$? +set -e + +# macdeployqt reports these optional, absent Qt frameworks as errors even +# though it succeeds. Preserve every other line so genuine failures remain. +filtered_macdeploy_output="$( + printf '%s\n' "${macdeploy_output}" | + sed -E \ + -e '/ERROR: Cannot resolve rpath "@rpath\/Qt(Pdf|Svg|VirtualKeyboard[A-Za-z]*)\.framework/d' \ + -e '/ERROR: using QList/d' +)" +if [[ -n "${filtered_macdeploy_output}" ]]; then + printf '%s\n' "${filtered_macdeploy_output}" +fi +if [[ ${macdeploy_result} -ne 0 ]]; then + echo "macdeployqt failed with exit code ${macdeploy_result}" >&2 + exit "${macdeploy_result}" +fi + +otool_dependencies() { + "${OTOOL}" -L "$1" | tail -n +2 +} + +# The first otool line is the inspected file's own path (normally under +# /Users/runner in CI), not a dependency, so exclude it from validation. +dependency_report="$(otool_dependencies "${lassie}")" +dependency_report+=$'\n' +dependency_report+="$(otool_dependencies "${cmod}")" +while IFS= read -r dylib; do + dependency_report+=$'\n' + dependency_report+="$(otool_dependencies "${dylib}")" +done < <(find "${frameworks}" -type f -name '*.dylib' -print) + +if grep -Eq '(/opt/homebrew|/usr/local|/opt/local|/Users)/' \ + <<<"${dependency_report}"; then + echo "DISSCO app still contains build-machine library paths:" >&2 + echo "${dependency_report}" >&2 + exit 1 +fi + +env -i HOME="${HOME:-/tmp}" PATH="/usr/bin:/bin" \ + "${cmod}" --help | grep -Fq "Usage:" + +echo "Validated ${APP_BUNDLE}" From 2545502a7b3dd3dccbc0e0c0c0a48776f5e53c26 Mon Sep 17 00:00:00 2001 From: DiyunZ Date: Mon, 24 Aug 2026 21:44:46 -0500 Subject: [PATCH 2/2] Fix LASSIE seed dialog title --- LASSIE/src/windows/MainWindow.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LASSIE/src/windows/MainWindow.cpp b/LASSIE/src/windows/MainWindow.cpp index 0878da88..e141fa1b 100644 --- a/LASSIE/src/windows/MainWindow.cpp +++ b/LASSIE/src/windows/MainWindow.cpp @@ -403,7 +403,7 @@ void MainWindow::runProject() } bool ok{}; - const QString seed = QInputDialog::getText(this, tr("QInputDialog::getText()"), + const QString seed = QInputDialog::getText(this, tr("Enter Seed"), tr("Enter a seed:"), QLineEdit::Normal, "abcd", &ok); if(!ok) return;