mirror of
https://github.com/cryptoadvance/specter-desktop.git
synced 2026-08-13 12:33:29 +02:00
fix: restore Intel macOS release compatibility
This commit is contained in:
parent
8843e2f064
commit
98c1e44186
12 changed files with 386 additions and 47 deletions
53
.github/workflows/electron-smoketest.yml
vendored
53
.github/workflows/electron-smoketest.yml
vendored
|
|
@ -4,11 +4,13 @@ on:
|
|||
pull_request:
|
||||
paths:
|
||||
- 'pyinstaller/electron/**'
|
||||
- '.github/workflows/release.yml'
|
||||
- '.github/workflows/electron-smoketest.yml'
|
||||
push:
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'pyinstaller/electron/**'
|
||||
- '.github/workflows/release.yml'
|
||||
- '.github/workflows/electron-smoketest.yml'
|
||||
|
||||
jobs:
|
||||
|
|
@ -48,10 +50,57 @@ jobs:
|
|||
node --check "$f"
|
||||
done
|
||||
|
||||
- name: Test release manifest validation
|
||||
working-directory: pyinstaller/electron
|
||||
run: npm test
|
||||
|
||||
- name: Boot electron, assert missing hash fails closed
|
||||
working-directory: pyinstaller/electron
|
||||
env:
|
||||
NODE_ENV: development
|
||||
SPECTER_DATA_FOLDER: ${{ runner.temp }}/specter_dev
|
||||
run: |
|
||||
mkdir -p "$RUNNER_TEMP/specter_dev"
|
||||
echo '{"version":"v2.1.11-pre1","repository":"cryptoadvance/specter-desktop","sha256":{"x64":"","arm64":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}}' \
|
||||
> "$RUNNER_TEMP/specter_dev/version-data.json"
|
||||
|
||||
xvfb-run --auto-servernum --server-args='-screen 0 1280x960x24' \
|
||||
./node_modules/.bin/electron . --no-sandbox > electron-invalid-manifest.log 2>&1 &
|
||||
PID=$!
|
||||
trap 'kill $PID 2>/dev/null || true; wait $PID 2>/dev/null || true' EXIT
|
||||
EXPECTED="does not contain a valid specterd hash for x64"
|
||||
|
||||
for _ in $(seq 1 30); do
|
||||
if grep -qF "$EXPECTED" electron-invalid-manifest.log; then
|
||||
if grep -qF 'Downloading from' electron-invalid-manifest.log; then
|
||||
echo "invalid manifest attempted a download"
|
||||
cat electron-invalid-manifest.log
|
||||
exit 1
|
||||
fi
|
||||
if grep -qF 'ReferenceError' electron-invalid-manifest.log; then
|
||||
echo "invalid manifest raised an uncaught reference error"
|
||||
cat electron-invalid-manifest.log
|
||||
exit 1
|
||||
fi
|
||||
echo "missing hash failed closed with an actionable error"
|
||||
exit 0
|
||||
fi
|
||||
if ! kill -0 $PID 2>/dev/null; then
|
||||
echo "electron exited before reporting the invalid manifest"
|
||||
cat electron-invalid-manifest.log
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "invalid manifest error not seen within 30s"
|
||||
cat electron-invalid-manifest.log
|
||||
exit 1
|
||||
|
||||
- name: Seed dev data folder
|
||||
run: |
|
||||
mkdir -p "$RUNNER_TEMP/specter_dev"
|
||||
echo '{"version":"","sha256":{"x64":"","arm64":""}}' \
|
||||
echo '{"version":"","repository":"cryptoadvance/specter-desktop","sha256":{"x64":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","arm64":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}}' \
|
||||
> "$RUNNER_TEMP/specter_dev/version-data.json"
|
||||
|
||||
- name: Boot electron, assert splash reaches no-specterd branch
|
||||
|
|
@ -71,7 +120,7 @@ jobs:
|
|||
# If main.js changes this message, update both sides together.
|
||||
EXPECTED="Specterd was not found and no version is configured"
|
||||
|
||||
for i in $(seq 1 30); do
|
||||
for _ in $(seq 1 30); do
|
||||
if grep -qF "$EXPECTED" electron.log; then
|
||||
echo "smoketest passed (splash reached no-specterd branch)"
|
||||
exit 0
|
||||
|
|
|
|||
101
.github/workflows/release.yml
vendored
101
.github/workflows/release.yml
vendored
|
|
@ -172,14 +172,14 @@ jobs:
|
|||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
# x64 builds require a paid macOS runner (macos-13 retired, macos-15-large is paid)
|
||||
# Uncomment when org has paid plan:
|
||||
# - runner: macos-15-large
|
||||
# arch: x64
|
||||
# arch_label: osx_x64
|
||||
- runner: macos-15-intel
|
||||
arch: x64
|
||||
arch_label: osx_x64
|
||||
file_arch: x86_64
|
||||
- runner: macos-14 # Apple Silicon (free tier)
|
||||
arch: arm64
|
||||
arch_label: osx_arm64
|
||||
file_arch: arm64
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
|
|
@ -217,6 +217,13 @@ jobs:
|
|||
pyinstaller specterd.spec
|
||||
cd ..
|
||||
|
||||
- name: Smoke test native specterd
|
||||
run: |
|
||||
FILE_OUTPUT=$(file pyinstaller/dist/specterd)
|
||||
echo "$FILE_OUTPUT"
|
||||
echo "$FILE_OUTPUT" | grep -q '${{ matrix.file_arch }}'
|
||||
"pyinstaller/dist/specterd" --help
|
||||
|
||||
- name: Package release
|
||||
run: |
|
||||
VERSION=${GITHUB_REF#refs/tags/}
|
||||
|
|
@ -441,6 +448,12 @@ jobs:
|
|||
name: specterd-macos-arm64
|
||||
path: ./release-artifacts
|
||||
|
||||
- name: Download specterd macOS x64 artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: specterd-macos-x64
|
||||
path: ./release-artifacts
|
||||
|
||||
- name: Cache Electron binaries
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
|
|
@ -482,13 +495,27 @@ jobs:
|
|||
run: |
|
||||
VERSION=${GITHUB_REF#refs/tags/}
|
||||
source .buildenv/bin/activate
|
||||
mkdir -p pyinstaller/dist
|
||||
mkdir -p pyinstaller/dist/arm64 pyinstaller/dist/x64
|
||||
cd release-artifacts
|
||||
unzip specterd-${VERSION}-osx_arm64.zip -d ../pyinstaller/dist/
|
||||
unzip "specterd-${VERSION}-osx_arm64.zip" -d ../pyinstaller/dist/arm64
|
||||
unzip "specterd-${VERSION}-osx_x64.zip" -d ../pyinstaller/dist/x64
|
||||
cd ..
|
||||
cd pyinstaller/electron
|
||||
npm ci
|
||||
node ./set-version $VERSION ../dist/specterd
|
||||
node ./set-version.js "$VERSION" ../dist/arm64/specterd arm64 "$GITHUB_REPOSITORY"
|
||||
node ./set-version.js "$VERSION" ../dist/x64/specterd x64 "$GITHUB_REPOSITORY"
|
||||
node -e "
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs');
|
||||
const { isValidSha256 } = require('./src/version-data');
|
||||
const data = require('./version-data.json');
|
||||
for (const arch of ['arm64', 'x64']) {
|
||||
if (!isValidSha256(data.sha256[arch])) throw new Error('Missing valid ' + arch + ' specterd hash');
|
||||
const binary = fs.readFileSync('../dist/' + arch + '/specterd');
|
||||
const actual = crypto.createHash('sha256').update(binary).digest('hex');
|
||||
if (data.sha256[arch] !== actual) throw new Error('Incorrect ' + arch + ' specterd hash');
|
||||
}
|
||||
"
|
||||
cd ../..
|
||||
|
||||
- name: Patch electron-builder config for CI
|
||||
|
|
@ -533,17 +560,44 @@ jobs:
|
|||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
|
||||
- name: Smoke test universal Electron package
|
||||
run: |
|
||||
APP_PATH=pyinstaller/electron/dist/mac-universal/Specter.app
|
||||
ELECTRON_BINARY="$APP_PATH/Contents/MacOS/Specter"
|
||||
ASAR_PATH="$APP_PATH/Contents/Resources/app.asar"
|
||||
|
||||
ARCHS=$(lipo -archs "$ELECTRON_BINARY")
|
||||
echo "Electron architectures: $ARCHS"
|
||||
echo "$ARCHS" | grep -qw x86_64
|
||||
echo "$ARCHS" | grep -qw arm64
|
||||
|
||||
ASAR_CONTENTS=$(pyinstaller/electron/node_modules/.bin/asar list "$ASAR_PATH")
|
||||
echo "$ASAR_CONTENTS" | grep -qx '/src/splash.html'
|
||||
echo "$ASAR_CONTENTS" | grep -qx '/version-data.json'
|
||||
node -e "
|
||||
const asar = require('./pyinstaller/electron/node_modules/@electron/asar');
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs');
|
||||
const data = JSON.parse(asar.extractFile(process.argv[1], 'version-data.json').toString());
|
||||
if (data.repository !== process.env.GITHUB_REPOSITORY) throw new Error('Incorrect packaged repository');
|
||||
for (const arch of ['arm64', 'x64']) {
|
||||
const binary = fs.readFileSync('pyinstaller/dist/' + arch + '/specterd');
|
||||
const expected = crypto.createHash('sha256').update(binary).digest('hex');
|
||||
if (data.sha256[arch] !== expected) throw new Error('Incorrect packaged ' + arch + ' specterd hash');
|
||||
}
|
||||
" "$ASAR_PATH"
|
||||
|
||||
- name: Package release
|
||||
run: |
|
||||
VERSION=${GITHUB_REF#refs/tags/}
|
||||
mkdir -p release
|
||||
cp pyinstaller/electron/dist/Specter-*.dmg release/Specter-${VERSION}.dmg 2>/dev/null || true
|
||||
# Fallback: if no DMG, package the .app
|
||||
if [ ! -f release/Specter-${VERSION}.dmg ]; then
|
||||
cd pyinstaller/electron/dist/mac-universal
|
||||
zip -r ../../../../release/Specter-${VERSION}-macos.zip Specter.app
|
||||
cd ../../../..
|
||||
DMG_FILES=(pyinstaller/electron/dist/Specter-*.dmg)
|
||||
if [ "${#DMG_FILES[@]}" -ne 1 ] || [ ! -f "${DMG_FILES[0]}" ]; then
|
||||
echo "Expected exactly one universal DMG, found: ${DMG_FILES[*]}"
|
||||
exit 1
|
||||
fi
|
||||
cp "${DMG_FILES[0]}" "release/Specter-${VERSION}.dmg"
|
||||
test -s "release/Specter-${VERSION}.dmg"
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
|
|
@ -587,9 +641,21 @@ jobs:
|
|||
find artifacts/pip-package -type f -name "cryptoadvance_specter-*.tar.gz" \
|
||||
-exec cp {} release-files/ \; 2>/dev/null || true
|
||||
|
||||
# A universal macOS release is incomplete without both native daemons
|
||||
# and the DMG that carries their hashes.
|
||||
for REQUIRED in \
|
||||
"specterd-${VERSION}-osx_x64.zip" \
|
||||
"specterd-${VERSION}-osx_arm64.zip" \
|
||||
"Specter-${VERSION}.dmg"; do
|
||||
if [ ! -s "release-files/${REQUIRED}" ]; then
|
||||
echo "Missing required macOS release artifact: ${REQUIRED}"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Generate SHA256SUMS
|
||||
cd release-files
|
||||
sha256sum * > SHA256SUMS
|
||||
sha256sum ./* > SHA256SUMS
|
||||
echo "=== SHA256SUMS ==="
|
||||
cat SHA256SUMS
|
||||
cd ..
|
||||
|
|
@ -619,7 +685,7 @@ jobs:
|
|||
- name: Generate release body
|
||||
run: |
|
||||
VERSION=${GITHUB_REF#refs/tags/}
|
||||
DL="https://github.com/cryptoadvance/specter-desktop/releases/download/${VERSION}"
|
||||
DL="https://github.com/${GITHUB_REPOSITORY}/releases/download/${VERSION}"
|
||||
|
||||
cat > release-body.md << EOF
|
||||
*Please create a full backup* before migrating or any major internal changes like switching to an electrum based installation. You can easily create a backup in Settings --> Backup Specter (zip file).
|
||||
|
|
@ -647,7 +713,7 @@ jobs:
|
|||
## specterd
|
||||
|
||||
Specterd is a command-line program that runs only the Specter server, behaving like a traditional web application.
|
||||
Supported platforms: [Windows](${DL}/specterd-${VERSION}-win64.zip), [MacOS](${DL}/specterd-${VERSION}-osx_arm64.zip), [Linux (x86_64)](${DL}/specterd-${VERSION}-x86_64-linux-gnu.zip)
|
||||
Supported platforms: [Windows](${DL}/specterd-${VERSION}-win64.zip), [macOS (Apple Silicon)](${DL}/specterd-${VERSION}-osx_arm64.zip), [macOS (Intel)](${DL}/specterd-${VERSION}-osx_x64.zip), [Linux (x86_64)](${DL}/specterd-${VERSION}-x86_64-linux-gnu.zip)
|
||||
|
||||
## PyPi Packages
|
||||
|
||||
|
|
@ -700,6 +766,7 @@ jobs:
|
|||
files: release-files/*
|
||||
body_path: release-body.md
|
||||
draft: true # Draft first, review before publishing
|
||||
prerelease: ${{ contains(github.ref_name, '-pre') }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ git push upstream v1.13.1
|
|||
That's it. The `Release` workflow on GitHub Actions takes it from here:
|
||||
|
||||
- **`release-pip`** — builds the sdist/wheel and publishes to PyPI via trusted publishing.
|
||||
- **`build-specterd-{linux,windows,macos}`** — builds the `specterd` binary on each platform (macOS arm64 on the free `macos-14` runner).
|
||||
- **`build-specterd-{linux,windows,macos}`** — builds the `specterd` binary on each platform. macOS uses native `macos-14` (arm64) and `macos-15-intel` (x64) runners and smoke-tests each daemon before packaging.
|
||||
- **`build-electron-{linux,windows,macos}`** — builds the Electron apps using each platform's `specterd` artifact. Windows uses the public `electronuserland/builder:wine` image; macOS signs + notarizes if `APPLE_CERTIFICATE_BASE64` et al. are configured.
|
||||
- **`create-release`** — collects all artifacts, generates `SHA256SUMS`, signs it with the GPG key from the `GPG_PRIVATE_KEY` secret, generates a release body (with auto-generated "What's Changed" via `gh api .../generate-notes`), and creates a **draft** GitHub release.
|
||||
- **`trigger-docker`** — POSTs a repository-dispatch to `lncm/docker-specter-desktop` so Aaron's Docker build picks up the new tag (needs `AARON_TRIGGER` secret; skipped otherwise).
|
||||
|
|
@ -58,3 +58,38 @@ The script installs the markdown prerequisite, regenerates the GH-page and downl
|
|||
If something fails mid-pipeline, re-running individual jobs is safe — they `actions/download-artifact` from prior jobs and overwrite existing release assets via `softprops/action-gh-release`. If the draft release already has assets from a stale run, delete the draft and re-run `create-release`.
|
||||
|
||||
macOS builds are the most likely to fail due to Apple signing/notarization glitches. The workflow falls back to unsigned builds when `APPLE_CERTIFICATE_BASE64` is empty — useful for smoke-testing the pipeline on forks.
|
||||
|
||||
## Validate a release candidate on a fork
|
||||
|
||||
Do not create or push a tag until the candidate diff and local checks have been reviewed. Once local review is approved, use immutable pre-release tags on the selected validation fork; never move a pre-tag after GitHub Actions has tested it. The release manifest records `${{ github.repository }}`, so a fork-built app downloads its fork artifacts while the real upstream release continues to download from `cryptoadvance/specter-desktop`.
|
||||
|
||||
```bash
|
||||
git remote get-url origin
|
||||
# Confirm that origin is the intended validation fork, not upstream.
|
||||
git fetch upstream master
|
||||
git merge-base --is-ancestor upstream/master HEAD
|
||||
git status --short --branch
|
||||
git diff --check upstream/master...HEAD
|
||||
cd pyinstaller/electron
|
||||
npm test
|
||||
cd ../..
|
||||
CANDIDATE_TAG=v2.1.11-pre1
|
||||
if git rev-parse --verify "refs/tags/${CANDIDATE_TAG}"; then
|
||||
echo "Tag already exists locally: ${CANDIDATE_TAG}"
|
||||
exit 1
|
||||
fi
|
||||
git tag "${CANDIDATE_TAG}"
|
||||
git push origin "${CANDIDATE_TAG}"
|
||||
```
|
||||
|
||||
The `-preN` tag creates a draft GitHub prerelease. PyPI publishing is disabled outside `cryptoadvance/specter-desktop`, and absent Apple/GPG secrets exercise the unsigned fork fallback; that does not validate upstream signing or notarization.
|
||||
|
||||
Before accepting the candidate, record the workflow run URL and verify:
|
||||
|
||||
- both macOS daemon jobs report the expected native architecture and pass `specterd --help`;
|
||||
- `specterd-v2.1.11-preN-osx_x64.zip`, `specterd-v2.1.11-preN-osx_arm64.zip`, and `Specter-v2.1.11-preN.dmg` are present;
|
||||
- `SHA256SUMS` contains those three artifacts;
|
||||
- the published prerelease and embedded daemon repository point to the selected fork;
|
||||
- the unsigned DMG launches, downloads, verifies, and relaunches on both a real Intel Mac and a real Apple Silicon Mac, including an upgrade with existing settings.
|
||||
|
||||
If the candidate changes, create `v2.1.11-pre2` (then `pre3`, and so on) at the new reviewed commit. Upstream still requires a secret-enabled signing/notarization validation before publication.
|
||||
|
|
|
|||
|
|
@ -1,14 +1,24 @@
|
|||
|
||||
function orgName() {
|
||||
// This can be changed in order to make download possible from other github orgs
|
||||
return "cryptoadvance"
|
||||
const DEFAULT_REPOSITORY = 'cryptoadvance/specter-desktop'
|
||||
const REPOSITORY_PATTERN = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/
|
||||
|
||||
function repositoryName(repository = DEFAULT_REPOSITORY) {
|
||||
if (!REPOSITORY_PATTERN.test(repository)) {
|
||||
throw new Error(`Invalid specterd download repository: ${repository}`)
|
||||
}
|
||||
return repository
|
||||
}
|
||||
|
||||
function getDownloadLocation(version, platformname) {
|
||||
function orgName() {
|
||||
return DEFAULT_REPOSITORY.split('/')[0]
|
||||
}
|
||||
|
||||
function getDownloadLocation(version, platformname, arch = process.arch, repository = DEFAULT_REPOSITORY) {
|
||||
const releaseRepository = repositoryName(repository)
|
||||
if (platformname != "osx") {
|
||||
return `https://github.com/${orgName()}/specter-desktop/releases/download/${version}/specterd-${version}-${platformname}.zip`
|
||||
return `https://github.com/${releaseRepository}/releases/download/${version}/specterd-${version}-${platformname}.zip`
|
||||
}
|
||||
return `https://github.com/${orgName()}/specter-desktop/releases/download/${version}/specterd-${version}-${platformname}_${process.arch}.zip`
|
||||
return `https://github.com/${releaseRepository}/releases/download/${version}/specterd-${version}-${platformname}_${arch}.zip`
|
||||
}
|
||||
|
||||
function appName() {
|
||||
|
|
@ -18,6 +28,6 @@ function appName() {
|
|||
module.exports = {
|
||||
getDownloadLocation,
|
||||
appName,
|
||||
orgName
|
||||
orgName,
|
||||
repositoryName,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ const { downloadSpecterd, destroyProgressbar } = require('./src/download.js')
|
|||
const { startSpecterd, quitSpecterd } = require('./src/specterd.js')
|
||||
const { getFileHash, versionData, isDev, devFolder, isMac } = require('./src/helpers.js')
|
||||
const { getAppSettings } = require('./src/config.js')
|
||||
const { showError, updatingLoaderMsg, initMainWindow, loadUrl, initTray } = require('./src/uiHelpers.js')
|
||||
const { showError, updatingLoaderMsg, updateSpecterdStatus, initMainWindow, loadUrl, initTray } = require('./src/uiHelpers.js')
|
||||
const { hashesMatch, missingHashMessage, synchronizeSpecterdSettings } = require('./src/version-data.js')
|
||||
|
||||
// Quit again if there is no version-data in dev
|
||||
if (isDev && versionData === undefined) {
|
||||
|
|
@ -136,24 +137,30 @@ app.whenReady().then(() => {
|
|||
|
||||
setMainMenu()
|
||||
|
||||
loadUrl(`file://${__dirname}/splash.html`)
|
||||
loadUrl(`file://${__dirname}/src/splash.html`)
|
||||
|
||||
if (!fs.existsSync(specterdDirPath)) {
|
||||
logger.info('Creating specterd-binaries folder:' + specterdDirPath)
|
||||
fs.mkdirSync(specterdDirPath, { recursive: true })
|
||||
}
|
||||
|
||||
if (!appSettings.versionInitialized || appSettings.versionInitialized != versionData.version) {
|
||||
const settingsUpdate = synchronizeSpecterdSettings(appSettings, versionData, process.arch)
|
||||
if (!settingsUpdate.hash) {
|
||||
const errorMessage = missingHashMessage(versionData, process.arch)
|
||||
logger.error(errorMessage)
|
||||
updatingLoaderMsg(errorMessage)
|
||||
updateSpecterdStatus('Failed to validate this release...')
|
||||
return
|
||||
}
|
||||
|
||||
if (settingsUpdate.changed) {
|
||||
logger.info(`Updating ${appSettingsPath} : ${JSON.stringify(appSettings)}`)
|
||||
appSettings.specterdVersion = versionData.version
|
||||
appSettings.specterdHash = versionData.sha256[process.arch]
|
||||
appSettings.versionInitialized = versionData.version
|
||||
fs.writeFileSync(appSettingsPath, JSON.stringify(appSettings))
|
||||
}
|
||||
const specterdPath = specterdDirPath + '/' + appNameLower + 'd'
|
||||
if (fs.existsSync(specterdPath + (platformName == 'win64' ? '.exe' : ''))) {
|
||||
getFileHash(specterdPath + (platformName == 'win64' ? '.exe' : ''), function (specterdHash) {
|
||||
if (appSettings.specterdHash.toLowerCase() == specterdHash || appSettings.specterdHash == '') {
|
||||
if (hashesMatch(appSettings.specterdHash, specterdHash)) {
|
||||
startSpecterd(specterdPath)
|
||||
} else if (appSettings.specterdVersion != '') {
|
||||
updatingLoaderMsg('Specterd version could not be validated. Trying again to download the Specter binary ...')
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@
|
|||
"start": "export NODE_ENV=development && cp -R ../../src/cryptoadvance/specter/static/fonts ../../src/cryptoadvance/specter/static/output.css ../../src/cryptoadvance/specter/static/typography.css . && electron .",
|
||||
"pack": "electron-builder --dir",
|
||||
"dist": "electron-builder --publish=never",
|
||||
"postinstall": "electron-builder install-app-deps"
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"test": "node --test test/*.test.js"
|
||||
},
|
||||
"repository": "https://github.com/cryptoadvance/specter-desktop",
|
||||
"keywords": [
|
||||
|
|
@ -69,4 +70,4 @@
|
|||
"resolutions": {
|
||||
"**/request/form-data": "^2.5.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,17 @@
|
|||
const fs = require('fs');
|
||||
const crypto = require('crypto');
|
||||
const versionDataFile = './version-data.json';
|
||||
const { isValidSha256 } = require('./src/version-data');
|
||||
|
||||
async function setVersion() {
|
||||
const version = process.argv[2];
|
||||
const file = process.argv[3];
|
||||
const arch = process.argv[4] || process.arch;
|
||||
const repository = process.argv[5] || process.env.GITHUB_REPOSITORY || 'cryptoadvance/specter-desktop';
|
||||
|
||||
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository)) {
|
||||
throw new Error(`Invalid release repository: ${repository}`);
|
||||
}
|
||||
|
||||
// Set version in package.json
|
||||
let packageJson = require('./package.json');
|
||||
|
|
@ -21,15 +27,20 @@ async function setVersion() {
|
|||
if (versionData.version != version) {
|
||||
console.log(`Version mismatch. Deleting ${versionDataFile} and creating anew.`);
|
||||
fs.unlinkSync(versionDataFile); // Delete the existing version-data.json file
|
||||
versionData = createNewVersionData(version); // Create new version data object
|
||||
versionData = createNewVersionData(version, repository); // Create new version data object
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.log(`No ${versionDataFile} found. Creating anew.`);
|
||||
versionData = createNewVersionData(version);
|
||||
versionData = createNewVersionData(version, repository);
|
||||
}
|
||||
// Compute SHA256 hash of the provided file
|
||||
versionData.sha256[arch] = await createHashFromFile(file);
|
||||
const fileHash = await createHashFromFile(file);
|
||||
if (!isValidSha256(fileHash)) {
|
||||
throw new Error(`Could not generate a valid SHA-256 hash for ${file}`);
|
||||
}
|
||||
versionData.sha256[arch] = fileHash;
|
||||
versionData.repository = repository;
|
||||
// Write new version data to file
|
||||
fs.writeFileSync(versionDataFile, JSON.stringify(versionData, undefined, 2));
|
||||
console.log("version-data.js: ")
|
||||
|
|
@ -41,10 +52,11 @@ async function setVersion() {
|
|||
}
|
||||
}
|
||||
|
||||
function createNewVersionData(version) {
|
||||
function createNewVersionData(version, repository) {
|
||||
// Return a new version data object
|
||||
return {
|
||||
version,
|
||||
repository,
|
||||
sha256: {}
|
||||
};
|
||||
}
|
||||
|
|
@ -57,4 +69,11 @@ const createHashFromFile = filePath => new Promise((resolve, reject) => {
|
|||
.on('error', reject);
|
||||
});
|
||||
|
||||
setVersion().catch(console.error);
|
||||
if (require.main === module) {
|
||||
setVersion().catch(error => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { createHashFromFile, createNewVersionData, setVersion };
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
const os = require('os')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const { getSpecterdHash } = require('./version-data')
|
||||
|
||||
|
||||
const downloadloc = require('../downloadloc');
|
||||
|
|
@ -44,7 +45,7 @@ function getAppSettings() {
|
|||
tor: false,
|
||||
proxyURL: "socks5://127.0.0.1:9050",
|
||||
specterdVersion: (versionData && versionData.version !== undefined) ? versionData.version : 'unknown',
|
||||
specterdHash: (versionData && versionData.sha256 !== undefined) ? versionData.sha256[process.arch] : 'unknown',
|
||||
specterdHash: getSpecterdHash(versionData, process.arch),
|
||||
specterdCLIArgs: "",
|
||||
versionInitialized: false
|
||||
}
|
||||
|
|
@ -107,4 +108,4 @@ module.exports = {
|
|||
getAppSettings,
|
||||
isDev: isDev,
|
||||
devFolder,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,12 +3,13 @@ const fs = require('fs')
|
|||
const { app, Menu } = require('electron')
|
||||
const extract = require('extract-zip')
|
||||
const { getDownloadLocation } = require('../downloadloc.js')
|
||||
const { appName, appSettings, platformName, appNameLower, versionDataPath } = require('./config.js')
|
||||
const { appName, appSettings, platformName, appNameLower, versionData, versionDataPath } = require('./config.js')
|
||||
const { isMac, getFileHash } = require('./helpers.js')
|
||||
const { logger } = require('./logging.js')
|
||||
const ProgressBar = require('electron-progressbar')
|
||||
const { updateSpecterdStatus, updatingLoaderMsg, createProgressBar } = require('./uiHelpers.js')
|
||||
const { startSpecterd } = require('./specterd.js')
|
||||
const { hashesMatch, isValidSha256, missingHashMessage } = require('./version-data.js')
|
||||
|
||||
let progressBar
|
||||
|
||||
|
|
@ -29,12 +30,25 @@ const dockMenuWithforceQuit = Menu.buildFromTemplate([
|
|||
])
|
||||
|
||||
function downloadSpecterd(specterdPath) {
|
||||
if (!isValidSha256(appSettings.specterdHash)) {
|
||||
const errorMessage = missingHashMessage({ version: appSettings.specterdVersion }, process.arch)
|
||||
logger.error(errorMessage)
|
||||
updatingLoaderMsg(errorMessage)
|
||||
updateSpecterdStatus('Failed to validate this release...')
|
||||
return
|
||||
}
|
||||
|
||||
updatingLoaderMsg(`Starting download`)
|
||||
updateSpecterdStatus(`Downloading the ${appName} binary...`)
|
||||
// Some logging
|
||||
logger.info('Using version ' + appSettings.specterdVersion)
|
||||
logger.info('Using platformName ' + platformName)
|
||||
download_location = getDownloadLocation(appSettings.specterdVersion, platformName)
|
||||
download_location = getDownloadLocation(
|
||||
appSettings.specterdVersion,
|
||||
platformName,
|
||||
process.arch,
|
||||
versionData.repository
|
||||
)
|
||||
logger.info('Downloading from ' + download_location)
|
||||
download(download_location, specterdPath + '.zip', function (errored, errorMsg) {
|
||||
if (errored == true) {
|
||||
|
|
@ -67,7 +81,7 @@ function downloadSpecterd(specterdPath) {
|
|||
fs.unlinkSync(specterdPath + '.zip')
|
||||
fs.rmdirSync(specterdPath + '-dir', { recursive: true })
|
||||
getFileHash(specterdPath + (platformName == 'win64' ? '.exe' : ''), function (specterdHash) {
|
||||
if (appSettings.specterdHash.toLowerCase() === specterdHash || appSettings.specterdHash == '') {
|
||||
if (hashesMatch(appSettings.specterdHash, specterdHash)) {
|
||||
startSpecterd(specterdPath)
|
||||
} else {
|
||||
updatingLoaderMsg('Specterd version could not be validated.')
|
||||
|
|
|
|||
47
pyinstaller/electron/src/version-data.js
Normal file
47
pyinstaller/electron/src/version-data.js
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
const SHA256_PATTERN = /^[0-9a-f]{64}$/i
|
||||
|
||||
function isValidSha256(value) {
|
||||
return typeof value === 'string' && SHA256_PATTERN.test(value)
|
||||
}
|
||||
|
||||
function getSpecterdHash(versionData, arch) {
|
||||
const hash = versionData && versionData.sha256 && versionData.sha256[arch]
|
||||
return isValidSha256(hash) ? hash.toLowerCase() : null
|
||||
}
|
||||
|
||||
function hashesMatch(expectedHash, actualHash) {
|
||||
return isValidSha256(expectedHash) && isValidSha256(actualHash) && expectedHash.toLowerCase() === actualHash.toLowerCase()
|
||||
}
|
||||
|
||||
function synchronizeSpecterdSettings(appSettings, versionData, arch) {
|
||||
const hash = getSpecterdHash(versionData, arch)
|
||||
if (!hash) {
|
||||
return { changed: false, hash: null }
|
||||
}
|
||||
|
||||
const changed =
|
||||
appSettings.specterdVersion !== versionData.version ||
|
||||
appSettings.versionInitialized !== versionData.version ||
|
||||
appSettings.specterdHash !== hash
|
||||
|
||||
if (changed) {
|
||||
appSettings.specterdVersion = versionData.version
|
||||
appSettings.specterdHash = hash
|
||||
appSettings.versionInitialized = versionData.version
|
||||
}
|
||||
|
||||
return { changed, hash }
|
||||
}
|
||||
|
||||
function missingHashMessage(versionData, arch) {
|
||||
const version = versionData && versionData.version ? ` ${versionData.version}` : ''
|
||||
return `Specter release${version} does not contain a valid specterd hash for ${arch}. Please install a complete release for this platform architecture.`
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getSpecterdHash,
|
||||
hashesMatch,
|
||||
isValidSha256,
|
||||
missingHashMessage,
|
||||
synchronizeSpecterdSettings,
|
||||
}
|
||||
23
pyinstaller/electron/test/downloadloc.test.js
Normal file
23
pyinstaller/electron/test/downloadloc.test.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
const assert = require('node:assert/strict')
|
||||
const test = require('node:test')
|
||||
|
||||
const { getDownloadLocation, repositoryName } = require('../downloadloc')
|
||||
|
||||
test('builds repository-relative Intel and ARM macOS daemon URLs', () => {
|
||||
assert.equal(
|
||||
getDownloadLocation('v2.1.11-pre1', 'osx', 'x64', 'example/specter-desktop'),
|
||||
'https://github.com/example/specter-desktop/releases/download/v2.1.11-pre1/specterd-v2.1.11-pre1-osx_x64.zip'
|
||||
)
|
||||
assert.equal(
|
||||
getDownloadLocation('v2.1.11-pre1', 'osx', 'arm64', 'example/specter-desktop'),
|
||||
'https://github.com/example/specter-desktop/releases/download/v2.1.11-pre1/specterd-v2.1.11-pre1-osx_arm64.zip'
|
||||
)
|
||||
})
|
||||
|
||||
test('keeps upstream as the compatibility default', () => {
|
||||
assert.equal(repositoryName(), 'cryptoadvance/specter-desktop')
|
||||
})
|
||||
|
||||
test('rejects malformed repository metadata', () => {
|
||||
assert.throws(() => repositoryName('https://example.com/attacker/repo'), /Invalid specterd download repository/)
|
||||
})
|
||||
66
pyinstaller/electron/test/version-data.test.js
Normal file
66
pyinstaller/electron/test/version-data.test.js
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
const assert = require('node:assert/strict')
|
||||
const test = require('node:test')
|
||||
|
||||
const {
|
||||
getSpecterdHash,
|
||||
hashesMatch,
|
||||
isValidSha256,
|
||||
missingHashMessage,
|
||||
synchronizeSpecterdSettings,
|
||||
} = require('../src/version-data')
|
||||
|
||||
const X64_HASH = 'a'.repeat(64)
|
||||
const ARM64_HASH = 'B'.repeat(64)
|
||||
|
||||
test('selects and normalizes the hash for each Electron architecture', () => {
|
||||
const versionData = { version: 'v2.1.11-pre1', sha256: { x64: X64_HASH, arm64: ARM64_HASH } }
|
||||
|
||||
assert.equal(getSpecterdHash(versionData, 'x64'), X64_HASH)
|
||||
assert.equal(getSpecterdHash(versionData, 'arm64'), ARM64_HASH.toLowerCase())
|
||||
})
|
||||
|
||||
test('rejects missing, empty, malformed, and non-string architecture hashes', () => {
|
||||
for (const hash of [undefined, null, '', 'unknown', 'a'.repeat(63), 42]) {
|
||||
assert.equal(getSpecterdHash({ sha256: { x64: hash } }, 'x64'), null)
|
||||
assert.equal(isValidSha256(hash), false)
|
||||
}
|
||||
})
|
||||
|
||||
test('hash comparison fails closed unless both values are valid SHA-256 strings', () => {
|
||||
assert.equal(hashesMatch(X64_HASH.toUpperCase(), X64_HASH), true)
|
||||
assert.equal(hashesMatch('', ''), false)
|
||||
assert.equal(hashesMatch(undefined, X64_HASH), false)
|
||||
assert.equal(hashesMatch(X64_HASH, 'not-a-hash'), false)
|
||||
})
|
||||
|
||||
test('missing hash errors identify the release and host architecture', () => {
|
||||
assert.match(missingHashMessage({ version: 'v2.1.11-pre1' }, 'x64'), /v2\.1\.11-pre1/)
|
||||
assert.match(missingHashMessage({ version: 'v2.1.11-pre1' }, 'x64'), /x64/)
|
||||
})
|
||||
|
||||
test('refreshes stale launcher settings without disturbing unrelated user settings', () => {
|
||||
const settings = {
|
||||
basicAuthUser: 'alice',
|
||||
specterdHash: null,
|
||||
specterdVersion: 'v2.1.10',
|
||||
versionInitialized: 'v2.1.10',
|
||||
}
|
||||
const versionData = { version: 'v2.1.11-pre1', sha256: { x64: X64_HASH } }
|
||||
|
||||
assert.deepEqual(synchronizeSpecterdSettings(settings, versionData, 'x64'), { changed: true, hash: X64_HASH })
|
||||
assert.equal(settings.basicAuthUser, 'alice')
|
||||
assert.equal(settings.specterdHash, X64_HASH)
|
||||
assert.equal(settings.specterdVersion, 'v2.1.11-pre1')
|
||||
assert.equal(settings.versionInitialized, 'v2.1.11-pre1')
|
||||
})
|
||||
|
||||
test('does not mutate settings when the release hash is missing', () => {
|
||||
const settings = { specterdHash: 'stale', specterdVersion: 'v2.1.10' }
|
||||
const original = { ...settings }
|
||||
|
||||
assert.deepEqual(synchronizeSpecterdSettings(settings, { version: 'v2.1.11', sha256: {} }, 'x64'), {
|
||||
changed: false,
|
||||
hash: null,
|
||||
})
|
||||
assert.deepEqual(settings, original)
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue