Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 178 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,18 @@ jobs:
else
VERSION="snapshot-${GITHUB_SHA::7}"
fi
TRIMMED_VERSION="${VERSION#v}"
if [[ "$TRIMMED_VERSION" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
PACKAGE_VERSION="$TRIMMED_VERSION"
MSIX_VERSION="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.${BASH_REMATCH[3]}.0"
Comment thread
JosunLP marked this conversation as resolved.
Outdated
else
echo "::warning::Release version '$VERSION' is not a plain semver tag; native package metadata falls back to placeholder versions."
PACKAGE_VERSION="0.0.0"
MSIX_VERSION="0.0.0.0"
Comment thread
JosunLP marked this conversation as resolved.
Outdated
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "package_version=$PACKAGE_VERSION" >> "$GITHUB_OUTPUT"
echo "msix_version=$MSIX_VERSION" >> "$GITHUB_OUTPUT"

- name: Build release binary
run: cargo build --release
Expand All @@ -68,18 +79,79 @@ jobs:
chmod +x "$BUNDLE_DIR/sort_it_now"
cp README.md "$BUNDLE_DIR/"
cp scripts/install-unix.sh "$BUNDLE_DIR/install.sh"
cp scripts/uninstall-unix.sh "$BUNDLE_DIR/uninstall.sh"
chmod +x "$BUNDLE_DIR/install.sh"
chmod +x "$BUNDLE_DIR/uninstall.sh"
tar -czf "$ARCHIVE_PATH" "$BUNDLE_DIR"
if [[ "$RUNNER_OS" == "Linux" ]]; then
sha256sum "$ARCHIVE_PATH" > "$ARCHIVE_PATH.sha256"
else
shasum -a 256 "$ARCHIVE_PATH" | awk '{printf "%s %s\n", $1, $2}' > "$ARCHIVE_PATH.sha256"
fi

- name: Package native installer (.deb)
if: matrix.os == 'ubuntu-latest'
env:
VERSION: ${{ steps.meta.outputs.version }}
PACKAGE_VERSION: ${{ steps.meta.outputs.package_version }}
SUFFIX: ${{ matrix.artifact_suffix }}
shell: bash
run: |
set -eux
PKG_ROOT="pkgroot"
mkdir -p "$PKG_ROOT/DEBIAN" "$PKG_ROOT/usr/local/bin" "$PKG_ROOT/usr/local/share/doc/sort-it-now"
cp target/release/sort_it_now "$PKG_ROOT/usr/local/bin/"
chmod 755 "$PKG_ROOT/usr/local/bin/sort_it_now"
cp README.md "$PKG_ROOT/usr/local/share/doc/sort-it-now/README.md"
Comment thread
JosunLP marked this conversation as resolved.
Outdated
cat > "$PKG_ROOT/DEBIAN/control" <<EOF
Package: sort-it-now
Version: ${PACKAGE_VERSION}
Section: utils
Priority: optional
Architecture: amd64
Maintainer: JosunLP <[email protected]>
Description: 3D box packing optimizer with web UI
Sort-it-now optimizes cuboid placement with stability, weight,
balance and live visualization support.
EOF
cat > "$PKG_ROOT/DEBIAN/prerm" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
rm -f /usr/local/bin/sort_it_now
EOF
chmod 755 "$PKG_ROOT/DEBIAN/prerm"
Comment thread
JosunLP marked this conversation as resolved.
Outdated
DEB_PATH="sort-it-now-${VERSION}-${SUFFIX}.deb"
dpkg-deb --build "$PKG_ROOT" "$DEB_PATH"
Comment thread
JosunLP marked this conversation as resolved.
Outdated
sha256sum "$DEB_PATH" > "$DEB_PATH.sha256"

- name: Package native installer (.pkg)
if: startsWith(matrix.os, 'macos')
env:
VERSION: ${{ steps.meta.outputs.version }}
PACKAGE_VERSION: ${{ steps.meta.outputs.package_version }}
SUFFIX: ${{ matrix.artifact_suffix }}
shell: bash
run: |
set -eux
PKG_ROOT="pkgroot"
mkdir -p "$PKG_ROOT/usr/local/bin" "$PKG_ROOT/usr/local/share/doc/sort-it-now"
cp target/release/sort_it_now "$PKG_ROOT/usr/local/bin/"
chmod 755 "$PKG_ROOT/usr/local/bin/sort_it_now"
cp README.md "$PKG_ROOT/usr/local/share/doc/sort-it-now/README.md"
PKG_PATH="sort-it-now-${VERSION}-${SUFFIX}.pkg"
pkgbuild \
--root "$PKG_ROOT" \
--identifier "io.github.josunlp.sort-it-now" \
--version "${PACKAGE_VERSION}" \
--install-location "/" \
"$PKG_PATH"
shasum -a 256 "$PKG_PATH" | awk '{printf "%s %s\n", $1, $2}' > "$PKG_PATH.sha256"

- name: Package artifact (Windows)
if: matrix.os == 'windows-latest'
env:
VERSION: ${{ steps.meta.outputs.version }}
MSIX_VERSION: ${{ steps.meta.outputs.msix_version }}
SUFFIX: ${{ matrix.artifact_suffix }}
EXT: ${{ matrix.archive_extension }}
shell: pwsh
Expand All @@ -90,16 +162,122 @@ jobs:
Copy-Item -Path "target\release\sort_it_now.exe" -Destination (Join-Path $bundleDir "sort_it_now.exe")
Copy-Item -Path "README.md" -Destination (Join-Path $bundleDir "README.md")
Copy-Item -Path "scripts\install-windows.ps1" -Destination (Join-Path $bundleDir "install.ps1")
Copy-Item -Path "scripts\uninstall-windows.ps1" -Destination (Join-Path $bundleDir "uninstall.ps1")
Compress-Archive -Path $bundleDir -DestinationPath $archivePath -Force
Get-FileHash -Path $archivePath -Algorithm SHA256 | Select-Object -ExpandProperty Hash | Out-File -FilePath "$archivePath.sha256" -Encoding ASCII

$msixDir = Join-Path $PWD "msix"
$assetsDir = Join-Path $msixDir "Assets"
New-Item -ItemType Directory -Force -Path $msixDir, $assetsDir | Out-Null
Copy-Item -Path "target\release\sort_it_now.exe" -Destination (Join-Path $msixDir "sort_it_now.exe") -Force

Add-Type -AssemblyName System.Drawing
function New-SolidPng {
param(
[string]$Path,
[int]$Size
)

$bitmap = New-Object System.Drawing.Bitmap $Size, $Size
$graphics = [System.Drawing.Graphics]::FromImage($bitmap)
$graphics.Clear([System.Drawing.Color]::FromArgb(255, 35, 117, 192))
$brush = New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::White)
$font = New-Object System.Drawing.Font("Arial", [Math]::Max([int]($Size / 4), 10), [System.Drawing.FontStyle]::Bold)
$graphics.DrawString("SIN", $font, $brush, 4, [Math]::Max([int]($Size / 3), 4))
$bitmap.Save($Path, [System.Drawing.Imaging.ImageFormat]::Png)
$graphics.Dispose()
$brush.Dispose()
$font.Dispose()
$bitmap.Dispose()
}

New-SolidPng -Path (Join-Path $assetsDir "Square44x44Logo.png") -Size 44
New-SolidPng -Path (Join-Path $assetsDir "Square150x150Logo.png") -Size 150
New-SolidPng -Path (Join-Path $assetsDir "StoreLogo.png") -Size 50

@"
<?xml version="1.0" encoding="utf-8"?>
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:desktop6="http://schemas.microsoft.com/appx/manifest/desktop/windows10/6"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="uap desktop6 rescap">
<Identity Name="JosunLP.SortItNow" Publisher="CN=SortItNow" Version="$env:MSIX_VERSION" ProcessorArchitecture="x64" />
<Properties>
<DisplayName>Sort It Now</DisplayName>
<PublisherDisplayName>JosunLP</PublisherDisplayName>
<Description>3D box packing optimizer</Description>
<Logo>Assets\StoreLogo.png</Logo>
</Properties>
<Resources>
<Resource Language="en-us" />
</Resources>
<Dependencies>
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.26100.0" />
</Dependencies>
<Applications>
<Application Id="SortItNow" Executable="sort_it_now.exe" EntryPoint="Windows.FullTrustApplication">
<uap:VisualElements
DisplayName="Sort It Now"
Description="3D box packing optimizer"
BackgroundColor="#2375C0"
Square150x150Logo="Assets\Square150x150Logo.png"
Square44x44Logo="Assets\Square44x44Logo.png" />
<Extensions>
<desktop6:Extension Category="windows.fullTrustProcess" Executable="sort_it_now.exe" />
</Extensions>
</Application>
</Applications>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
</Capabilities>
</Package>
"@ | Out-File -FilePath (Join-Path $msixDir "AppxManifest.xml") -Encoding utf8

$kitBin = Get-ChildItem "C:\Program Files (x86)\Windows Kits\10\bin\*\x64" -Directory |
Sort-Object FullName -Descending |
Select-Object -First 1
if (-not $kitBin) {
throw "Could not locate Windows SDK tools for MSIX packaging."
}

$makeAppx = Join-Path $kitBin.FullName "makeappx.exe"
$signtool = Join-Path $kitBin.FullName "signtool.exe"
$msixPath = "sort-it-now-$env:VERSION-$env:SUFFIX.msix"
& $makeAppx pack /d $msixDir /p $msixPath /o

$cert = New-SelfSignedCertificate `
-Type Custom `
-Subject "CN=SortItNow" `
-KeyUsage DigitalSignature `
-FriendlyName "Sort It Now CI Signing" `
-TextExtension @("2.5.29.37={text}1.3.6.1.5.5.7.3.3") `
-CertStoreLocation "Cert:\CurrentUser\My"
$generatedPassword = [guid]::NewGuid().ToString("N")
$passwordSecure = ConvertTo-SecureString -String $generatedPassword -AsPlainText -Force
$pfxPath = Join-Path $PWD "sort-it-now-ci-signing.pfx"
$cerPath = "sort-it-now-$env:VERSION-$env:SUFFIX.cer"
Export-PfxCertificate -Cert $cert -FilePath $pfxPath -Password $passwordSecure | Out-Null
Export-Certificate -Cert $cert -FilePath $cerPath | Out-Null
& $signtool sign /fd SHA256 /f $pfxPath /p $generatedPassword $msixPath
Remove-Item -Path $pfxPath -Force
Get-FileHash -Path $msixPath -Algorithm SHA256 | Select-Object -ExpandProperty Hash | Out-File -FilePath "$msixPath.sha256" -Encoding ASCII

- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: sort-it-now-${{ steps.meta.outputs.version }}-${{ matrix.artifact_suffix }}
path: |
sort-it-now-${{ steps.meta.outputs.version }}-${{ matrix.artifact_suffix }}.${{ matrix.archive_extension }}
sort-it-now-${{ steps.meta.outputs.version }}-${{ matrix.artifact_suffix }}.${{ matrix.archive_extension }}.sha256
sort-it-now-${{ steps.meta.outputs.version }}-${{ matrix.artifact_suffix }}.deb
sort-it-now-${{ steps.meta.outputs.version }}-${{ matrix.artifact_suffix }}.deb.sha256
sort-it-now-${{ steps.meta.outputs.version }}-${{ matrix.artifact_suffix }}.pkg
sort-it-now-${{ steps.meta.outputs.version }}-${{ matrix.artifact_suffix }}.pkg.sha256
sort-it-now-${{ steps.meta.outputs.version }}-${{ matrix.artifact_suffix }}.msix
sort-it-now-${{ steps.meta.outputs.version }}-${{ matrix.artifact_suffix }}.msix.sha256
sort-it-now-${{ steps.meta.outputs.version }}-${{ matrix.artifact_suffix }}.cer
Comment thread
JosunLP marked this conversation as resolved.
Outdated

publish:
name: Publish GitHub Release
Expand Down
62 changes: 59 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,18 @@ An intelligent 3D packing optimization service with interactive visualization.
- **OrbitControls** for camera control
- **Container navigation** (Previous/Next buttons)
- **Step-by-step animation** of the packing process
- **Highlighted live/animation focus** for the current placement step
- **Live statistics**:
- Object count
- Total weight
- Volume utilization
- Center of mass position
- **Packing status panel** with progress and configuration readiness
- **Unplaced object panel** with rejection reasons
- **Configuration modal** with object rotation toggle
- **Persistent configuration** via browser local storage
- **Keyboard shortcuts** for batch/live runs, animation, navigation, and configuration
- **Inline validation and toast notifications** for faster feedback
- **Responsive design**

## 🚀 Installation & Startup
Expand All @@ -40,6 +46,7 @@ An intelligent 3D packing optimization service with interactive visualization.
- Rust (1.70+)
- Cargo
- Modern web browser
- Python 3 (only needed for the Unix one-command installer)

### Start the backend

Expand All @@ -55,27 +62,76 @@ The server runs on `http://localhost:8080`

The web client is automatically served by the Rust backend. After startup, simply open `http://localhost:8080/` in your browser.

> 🔗 **Same-origin note:** The frontend intentionally calls `/pack` and `/pack_stream` on the same origin that serves the UI. This matches the default local setup (`cargo run`) and the production deployment model where the Rust backend serves both API and web assets.

In the browser:

- Button "🚀 Pack (Batch)" performs a one-time optimization and displays the result.
- Button "📡 Pack (Live)" starts the live stream of optimization steps via SSE and renders them continuously.
- Saved configurations are restored automatically after a page reload.
- The status and unplaced-object panels provide immediate feedback without blocking dialogs.
- Keyboard shortcuts:
- `B` = batch packing
- `L` = live packing
- `C` = open configuration
- `←` / `→` = switch containers
- `Space` = start/stop animation

## 📦 Pre-built Releases & Release Pipeline

A GitHub Actions workflow (`.github/workflows/release.yml`) exists for releases that generates platform packages when tags in the format `v*` are created (or manually via _workflow_dispatch_):

- **Linux (x86_64)**: `sort-it-now-<version>-linux-x86_64.tar.gz`
- **Linux native installer**: `sort-it-now-<version>-linux-x86_64.deb`
- **macOS (ARM64/Apple Silicon)**: `sort-it-now-<version>-macos-arm64.tar.gz`
- **macOS (x86_64/Intel)**: `sort-it-now-<version>-macos-x86_64.tar.gz`
- **macOS native installer**: `sort-it-now-<version>-macos-<arch>.pkg`
- **Windows (x86_64)**: `sort-it-now-<version>-windows-x86_64.zip`
- **Windows native installer**: `sort-it-now-<version>-windows-x86_64.msix`

Each package contains the pre-compiled binary, the current `README.md`, and an installation script.
Each archive package contains the pre-compiled binary, the current `README.md`, and installation/uninstallation scripts.
The artifacts are uploaded both as workflow artifacts and automatically added to the GitHub release for the corresponding tag version.

### Installation Scripts
### Single-command Installation / Uninstallation

- Linux / macOS install:

```bash
curl -fsSL https://raw.githubusercontent.com/JosunLP/sort-it-now/main/scripts/install-unix.sh | bash
```

- Linux / macOS uninstall:

```bash
curl -fsSL https://raw.githubusercontent.com/JosunLP/sort-it-now/main/scripts/uninstall-unix.sh | bash
```

- Windows install (PowerShell):

```powershell
irm https://raw.githubusercontent.com/JosunLP/sort-it-now/main/scripts/install-windows.ps1 | iex
```

- Windows uninstall (PowerShell):

```powershell
irm https://raw.githubusercontent.com/JosunLP/sort-it-now/main/scripts/uninstall-windows.ps1 | iex
Comment thread
JosunLP marked this conversation as resolved.
Outdated
Comment thread
JosunLP marked this conversation as resolved.
Outdated
```

Both installer scripts also continue to work locally from an extracted release bundle. Set `INSTALL_DIR` (Unix) or `-Destination` (PowerShell) to override the default target.

### Archive Installation Scripts

- Linux/macOS: Run `./install.sh` in the extracted folder (optionally with `sudo`) to copy `sort_it_now` to `/usr/local/bin`.
- Linux/macOS: Run `./uninstall.sh` in the extracted folder to remove a prior archive-based installation again.
- Windows: Run `install.ps1` (PowerShell). By default, it installs to `%ProgramFiles%\sort-it-now` and adds the path to the user environment variable.
- Windows: Run `uninstall.ps1` to remove the installed binary and clean the user PATH entry again.

### Native Installer Notes

- **Linux (`.deb`)**: Install with `sudo dpkg -i sort-it-now-<version>-linux-x86_64.deb`, uninstall with `sudo dpkg -r sort-it-now`.
- **macOS (`.pkg`)**: Install with `sudo installer -pkg sort-it-now-<version>-macos-<arch>.pkg -target /`. Use the uninstall shell script afterwards if you want to remove the binary from `/usr/local/bin`.
- **Windows (`.msix`)**: The release workflow produces a signed MSIX together with the matching `.cer` certificate. Import the certificate into the trusted people store once, then install the package with `Add-AppxPackage .\sort-it-now-<version>-windows-x86_64.msix`. Only trust that certificate when the release came from the official repository and the published checksums were verified, because the package is signed with a repository-generated self-signed certificate.
Comment thread
JosunLP marked this conversation as resolved.
Outdated

### Docker

Expand Down Expand Up @@ -112,7 +168,7 @@ The server is then available at `http://localhost:8080`.

## 🔔 Automatic Updates on Startup

On startup, the service checks for the latest GitHub releases (`JosunLP/sort-it-now`) in the background. If a newer version is found, the updater downloads the appropriate release package and runs the installation script for the current platform. This automatically applies the update where possible. On Windows, if `sort_it_now.exe` is locked, a `sort_it_now.new.exe` is placed instead.
On startup, the service checks for the latest GitHub releases (`JosunLP/sort-it-now`) in the background. If a newer version is found, the updater downloads the archive package matching the current platform and updates the installed binary in place. Native installers (`.deb`, `.pkg`, `.msix`) are published alongside the archive assets for manual installation flows. On Windows, if `sort_it_now.exe` is locked, a `sort_it_now.new.exe` is placed instead.

- The check can be disabled via the environment variable `SORT_IT_NOW_SKIP_UPDATE_CHECK=1` (e.g., for offline installations or CI).
- GitHub limits unauthenticated API calls to 60 per hour. If the limit is reached, the check is skipped and info is displayed. Optionally set `SORT_IT_NOW_GITHUB_TOKEN` (or `GITHUB_TOKEN`) to a Personal Access Token to get higher limits; the updater also uses the token when downloading release artifacts.
Expand Down
Loading
Loading