Skip to content

MSIX and DMG installation script #38

Description

@JanisErdmanis

It is particularly annoying for the user to install self-signed MSIX and DMG packages, as there are multiple side steps one needs to take. To remedy the situation, one could instead distribute the application via a shell command that runs like:

curl trust.me/install.sh | sh

...with an analogous variant on Windows. Under the hood, the script would download the MSIX or DMG and then install it. This approach preserves system integration and clean uninstallation without duplicating maintenance effort.

For the JuliaCon presentation, with the help of AI I put together the following scripts:

#!/bin/bash

set -e
DMG="$1"

echo
echo "Mounting DMG..."

MOUNT_POINT=$(hdiutil attach "$DMG" -nobrowse | awk -F '\t' '/\/Volumes\// {print $NF; exit}')

if [ -z "$MOUNT_POINT" ]; then
    echo "Error: failed to mount DMG."
    exit 1
fi

echo "Mounted at: $MOUNT_POINT"

# Make sure the DMG is detached if the script exits unexpectedly
cleanup() {
    if [ -n "$MOUNT_POINT" ] && mount | grep -Fq "$MOUNT_POINT"; then
        hdiutil detach "$MOUNT_POINT" >/dev/null 2>&1 || true
    fi

    rm -f "$DMG"
}

trap cleanup EXIT

echo "Searching for application..."

APP=$(find "$MOUNT_POINT" -maxdepth 2 -type d -name "*.app" -print -quit)

if [ -z "$APP" ]; then
    echo "Error: no .app application found in the DMG."
    exit 1
fi

APP_NAME=$(basename "$APP")

echo "Found: $APP_NAME"
echo "Installing to /Applications..."

sudo rm -rf "/Applications/$APP_NAME"
sudo cp -R "$APP" "/Applications/$APP_NAME"

echo "Removing quarantine attribute..."

sudo xattr -dr com.apple.quarantine "/Applications/$APP_NAME"

echo
echo "Installation complete:"
echo "  /Applications/$APP_NAME"

And for Windows, the following PowerShell script:

param(
    [Parameter(Mandatory = $true, Position = 0)]
    [string]$Msix
)

$ErrorActionPreference = "Stop"

# ----------------------------------------------------------------------
# Resolve MSIX path
# ----------------------------------------------------------------------

$Msix = (Resolve-Path -LiteralPath $Msix).Path

Write-Host "Installing: $Msix"

# ----------------------------------------------------------------------
# Extract signing certificate
# ----------------------------------------------------------------------

$sig = Get-AuthenticodeSignature $Msix

if (-not $sig.SignerCertificate) {
    throw "The MSIX package does not contain a signing certificate."
}

# Uncomment if you want to reject packages whose signature isn't valid.
#
# if ($sig.Status -ne "Valid") {
#     throw "MSIX signature is not valid: $($sig.Status)"
# }

$cert = $sig.SignerCertificate
$thumbprint = $cert.Thumbprint

Write-Host "Signer: $($cert.Subject)"
Write-Host "Thumbprint: $thumbprint"

# ----------------------------------------------------------------------
# Export signing certificate
# ----------------------------------------------------------------------

$certPath = Join-Path `
    $env:TEMP `
    "AppBundler-$thumbprint.cer"

Export-Certificate `
    -Cert $cert `
    -FilePath $certPath `
    -Force |
    Out-Null

Write-Host "Certificate: $certPath"

# ----------------------------------------------------------------------
# Paths for elevated helper and log
# ----------------------------------------------------------------------

$scriptPath = Join-Path `
    $env:TEMP `
    "AppBundler-InstallCert-$thumbprint.ps1"

$logPath = Join-Path `
    $env:TEMP `
    "AppBundler-InstallCert-$thumbprint.log"

# ----------------------------------------------------------------------
# Create elevated certificate-installation script
# ----------------------------------------------------------------------

@'
param(
    [Parameter(Mandatory = $true)]
    [string]$CertPath,

    [Parameter(Mandatory = $true)]
    [string]$Thumbprint,

    [Parameter(Mandatory = $true)]
    [string]$LogPath
)

$ErrorActionPreference = "Stop"

function Write-Log {
    param(
        [string]$Message
    )

    $Message | Tee-Object -FilePath $LogPath -Append
}

try {

    $storePath = "Cert:\LocalMachine\TrustedPeople"

    Write-Log "Running elevated certificate installation..."
    Write-Log "Certificate: $CertPath"
    Write-Log "Expected thumbprint: $Thumbprint"
    Write-Log "Store: $storePath"

    $installed = Get-ChildItem $storePath |
        Where-Object {
            $_.Thumbprint -eq $Thumbprint
        }

    if ($installed) {

        Write-Log ""
        Write-Log "Certificate is already trusted."
        Write-Log "Subject:    $($installed.Subject)"
        Write-Log "Thumbprint: $($installed.Thumbprint)"
        Write-Log "Valid from: $($installed.NotBefore)"
        Write-Log "Valid to:   $($installed.NotAfter)"

        exit 0
    }

    if (-not (Test-Path -LiteralPath $CertPath)) {
        throw "Certificate file does not exist: $CertPath"
    }

    Write-Log ""
    Write-Log "Certificate file found."

    Write-Log "Importing certificate..."

    $imported = Import-Certificate `
        -FilePath $CertPath `
        -CertStoreLocation $storePath

    Write-Log "Certificate imported."
    Write-Log "Subject:    $($imported.Subject)"
    Write-Log "Thumbprint: $($imported.Thumbprint)"

    Write-Log ""
    Write-Log "Verifying certificate in $storePath..."

    $installed = Get-ChildItem $storePath |
        Where-Object {
            $_.Thumbprint -eq $Thumbprint
        }

    if (-not $installed) {
        throw "Certificate was NOT found in $storePath after import."
    }

    Write-Log ""
    Write-Log "Certificate successfully verified."
    Write-Log "Subject:    $($installed.Subject)"
    Write-Log "Thumbprint: $($installed.Thumbprint)"
    Write-Log "Valid from: $($installed.NotBefore)"
    Write-Log "Valid to:   $($installed.NotAfter)"

    exit 0
}
catch {
    Write-Log ""
    Write-Log "ERROR: $($_.Exception.Message)"
    exit 1
}
'@ | Set-Content `
    -LiteralPath $scriptPath `
    -Encoding UTF8

Remove-Item `
    -LiteralPath $logPath `
    -Force `
    -ErrorAction SilentlyContinue

try {

    Write-Host ""
    Write-Host "Checking/installing certificate in LocalMachine\TrustedPeople..."
    Write-Host "Log: $logPath"

    $process = Start-Process `
        -FilePath "powershell.exe" `
        -Verb RunAs `
        -WindowStyle Hidden `
        -ArgumentList @(
            "-NoProfile"
            "-ExecutionPolicy"
            "Bypass"
            "-File"
            $scriptPath
            $certPath
            $thumbprint
            $logPath
        ) `
        -Wait `
        -PassThru

    Write-Host ""
    Write-Host "Elevated process exit code: $($process.ExitCode)"

    if (Test-Path -LiteralPath $logPath) {

        Write-Host ""
        Write-Host "Certificate installer log:"
        Write-Host "----------------------------------------"

        Get-Content -LiteralPath $logPath

        Write-Host "----------------------------------------"
    }
    else {
        Write-Host "WARNING: Certificate installer did not produce a log."
    }

    if ($process.ExitCode -ne 0) {
        throw "Failed to install and verify the AppBundler certificate."
    }

    Write-Host ""
    Write-Host "Certificate installation completed successfully."
}
finally {

    Remove-Item `
        -LiteralPath $scriptPath `
        -Force `
        -ErrorAction SilentlyContinue
}

Write-Host ""
Write-Host "Launching App Installer..."

Start-Process $Msix

At the end, Start-Process $Msix can be replaced with Add-AppxPackage $Msix, which installs it directly from the terminal.

There are two remaining tasks to be done:

  • Check if the length of the scripts is justified
  • Put this in the documenation

It seems that each archive MSIX, DMG, Snap necesiates their own documenation entry basically mimicing the JuliaCon AppBundler presentation content https://janiserdmanis.org/artifacts/JuliaCon2026-AppBundler

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions