diff --git a/.github/workflows/linux-build.yml b/.github/workflows/linux-build.yml new file mode 100644 index 000000000..496970357 --- /dev/null +++ b/.github/workflows/linux-build.yml @@ -0,0 +1,79 @@ +# Linux build, unit tests, and the IPC open-forwarding regression gate. +# +# The existing workflows build Windows and generate documentation; nothing +# builds QElectroTech on Linux or runs tests/ in CI. This does both. +# +# It runs inside an ubuntu:26.04 container rather than directly on the runner +# because the IPC gate below needs Qt 6.10.2. The ubuntu-latest image ships +# Qt 6.4, and whether the crash that gate covers reproduces on 6.4 has never +# been checked -- a gate that cannot go red is worse than no gate. +name: Linux build and tests + +on: + push: + branches: [master] + pull_request: + workflow_dispatch: + +jobs: + linux: + name: Build and test (Qt 6, Debug) + runs-on: ubuntu-latest + container: ubuntu:26.04 + + steps: + - name: Install build and test dependencies + run: | + apt-get update + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + ca-certificates build-essential cmake ninja-build git pkg-config \ + qt6-base-dev qt6-base-private-dev qt6-tools-dev qt6-tools-dev-tools \ + libqt6svg6-dev libqt6sql6-sqlite libsqlite3-dev libcups2-dev \ + libxkbcommon-x11-0 \ + xvfb openbox xdotool x11-utils + # extra-cmake-modules and the KF6 libraries are installed rather than + # left to FetchContent: without them CMake builds extra-cmake-modules + # from source, and its Qt Help documentation target then demands + # Qt6{Core,Gui,Widgets,DBus}Tools, failing the configure with an error + # that appears to be about QElectroTech and is not. + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + extra-cmake-modules libkf6coreaddons-dev libkf6widgetsaddons-dev \ + qt6-svg-plugins + + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Configure + # Debug, not Release. Whether a use-after-free faults depends on what + # the allocator does with the freed block: the commit before #868 + # crashes every time built Debug and survives every time built + # -O3 -DNDEBUG, so a Release job would never catch a regression here. + run: | + git config --global --add safe.directory "$GITHUB_WORKSPACE" + cmake -B build -G Ninja \ + -DCMAKE_BUILD_TYPE=Debug \ + -DQT_VERSION_MAJOR=6 + + - name: Build + run: cmake --build build --parallel $(nproc) + + - name: Unit tests + run: xvfb-run -a ctest --test-dir build --output-on-failure + + - name: IPC open-forwarding regression (#868) + # Fails the job only on an actual crash. An inconclusive run means the + # crash path was not exercised -- it proves nothing, which is not the + # same as a regression, and failing on it would make this job flaky + # rather than informative. + run: | + set +e + tests/ipc-regression/run.sh --binary build/qelectrotech + status=$? + set -e + case $status in + 0) echo "::notice::IPC gate passed" ;; + 2) echo "::warning::IPC gate inconclusive -- the crash path was not exercised, nothing was proven" ;; + *) echo "::error::IPC gate failed -- forwarded files are being opened inside the socket handler again" + exit 1 ;; + esac diff --git a/.github/workflows/windows-msi.yml b/.github/workflows/windows-msi.yml index 73d57e7ab..03428963a 100644 --- a/.github/workflows/windows-msi.yml +++ b/.github/workflows/windows-msi.yml @@ -42,6 +42,9 @@ jobs: pages: write id-token: write # Required by SignPath + env: + SIGNPATH_API_TOKEN: ${{ secrets.SIGNPATH_API_TOKEN }} + outputs: qt6_msi: ${{ steps.export.outputs.msi_name_qt6 }} @@ -290,7 +293,8 @@ jobs: # PR/run must never attempt a signing request. # (cf. DieterMayerOSS:fix/msi-signing-fork-guard, d3f60c88) - name: Sign MSI via SignPath - if: github.repository == 'qelectrotech/qelectrotech-source-mirror' + id: sign + if: github.repository == 'qelectrotech/qelectrotech-source-mirror' && env.SIGNPATH_API_TOKEN != '' uses: signpath/github-action-submit-signing-request@v2 with: api-token: ${{ secrets.SIGNPATH_API_TOKEN }} @@ -345,7 +349,7 @@ jobs: run: | Write-Host "=== MSI build summary (${{ matrix.flavor }}) ===" Write-Host "Version : ${{ steps.version.outputs.VERSION_DISPLAY }}" - Write-Host "Signed : true" + Write-Host "Signed : ${{ steps.sign.outcome == 'success' }}" # --------------------------------------------------------------------------- # Job 2 : Génère et déploie la page GitHub Pages une fois le MSI publié, diff --git a/sources/diagramevent/diagrameventaddpaste.cpp b/sources/diagramevent/diagrameventaddpaste.cpp index 44fe49e24..ac29b522a 100644 --- a/sources/diagramevent/diagrameventaddpaste.cpp +++ b/sources/diagramevent/diagrameventaddpaste.cpp @@ -58,22 +58,30 @@ const QList movable = m_content.items(MovableItems); if (movable.isEmpty()) return; - //Compute the bounding rect centre — this is where the cursor - //will start. Items stay at their original XML positions; + //Compute the top-left of all items' positions (not bounding + //rects) and snap to grid — used only for the initial cursor + //warp. Items stay at their original XML positions; //moveTo() handles grid-snapped movement via deltas. - QRectF group_rect; + QPointF top_left; + bool first = true; for (auto *item : movable) { - group_rect = group_rect.united(item->mapToScene(item->boundingRect()).boundingRect()); + const QPointF p = item->pos(); + if (first) { + top_left = p; + first = false; + } else { + if (p.x() < top_left.x()) top_left.setX(p.x()); + if (p.y() < top_left.y()) top_left.setY(p.y()); + } } - QSettings settings; const int xGrid = settings.value(QStringLiteral("diagrameditor/Xgrid"), Diagram::xGrid).toInt(); const int yGrid = settings.value(QStringLiteral("diagrameditor/Ygrid"), Diagram::yGrid).toInt(); const QPointF grid_origin( - qRound(group_rect.topLeft().x() / xGrid) * xGrid, - qRound(group_rect.topLeft().y() / yGrid) * yGrid); + qRound(top_left.x() / xGrid) * xGrid, + qRound(top_left.y() / yGrid) * yGrid); //Store each item's position. moveTo() applies a grid-snapped //delta from the baseline, so items preserve their layout and