Merge branch 'signmacosdesktop' of https://github.com/ethereum/remix-project into desktope2e
commit
8c2cda5eb9
@ -0,0 +1,192 @@ |
||||
import {Octokit} from 'octokit' |
||||
import * as fs from 'fs' |
||||
import * as path from 'path' |
||||
import YAML from 'yaml' |
||||
import crypto from 'crypto' |
||||
|
||||
const owner = 'bunsenstraat' |
||||
const repo = 'remix-desktop' |
||||
const headers = { |
||||
'X-GitHub-Api-Version': '2022-11-28', |
||||
} |
||||
|
||||
const octokit = new Octokit({ |
||||
auth: process.env.GH_TOKEN, |
||||
}) |
||||
|
||||
async function getAllReleases() { |
||||
const releases = await octokit.request('GET /repos/{owner}/{repo}/releases', { |
||||
owner: owner, |
||||
repo: repo, |
||||
headers: headers, |
||||
}) |
||||
return releases.data |
||||
} |
||||
|
||||
async function uploadReleaseAsset(release, name, file) { |
||||
const upload_url = release.upload_url |
||||
console.log(`Uploading ${name} to ${upload_url}`) |
||||
octokit.request({ |
||||
method: "POST", |
||||
url: upload_url, |
||||
headers: { |
||||
"content-type": "text/plain", |
||||
}, |
||||
data: fs.readFileSync(file), |
||||
name, |
||||
label: name |
||||
}); |
||||
} |
||||
|
||||
async function getVersionFromPackageJson() { |
||||
// ignore ts error
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const packageJson = require(__dirname + '/../../../apps/remixdesktop/package.json') |
||||
return packageJson.version |
||||
} |
||||
|
||||
async function readReleaseFilesFromLocalDirectory() { |
||||
const directoryPath = path.join(__dirname, '../../../release') |
||||
const files = fs.readdirSync(directoryPath) |
||||
return files |
||||
} |
||||
|
||||
async function removeAsset(asset) { |
||||
await octokit.request('DELETE /repos/{owner}/{repo}/releases/assets/{asset_id}', { |
||||
owner: owner, |
||||
repo: repo, |
||||
asset_id: asset.id, |
||||
headers: headers, |
||||
}) |
||||
} |
||||
|
||||
async function hashFile(file): Promise<string> { |
||||
return new Promise((resolve, reject) => { |
||||
const hash = crypto.createHash('sha512').setEncoding('base64'); |
||||
// hash.on('error', reject).setEncoding(encoding);
|
||||
fs.createReadStream( |
||||
file, |
||||
Object.assign({}, {}, { |
||||
highWaterMark: 1024 * 1024, |
||||
/* better to use more memory but hash faster */ |
||||
}) |
||||
) |
||||
.on('error', reject) |
||||
.on('end', () => { |
||||
hash.end(); |
||||
console.log('hash done'); |
||||
console.log(hash.read()); |
||||
resolve(hash.digest('base64')); |
||||
}) |
||||
.pipe( |
||||
hash, |
||||
{ |
||||
end: false, |
||||
} |
||||
); |
||||
}); |
||||
} |
||||
|
||||
async function main() { |
||||
const allReleases = await getAllReleases() |
||||
const version = await getVersionFromPackageJson() |
||||
console.log(`preparing release version: ${version}`) |
||||
let release |
||||
allReleases.find((r) => { |
||||
if (r.tag_name === `v${version}`) { |
||||
release = r |
||||
} |
||||
}) |
||||
if (!release) { |
||||
console.log('No release found.') |
||||
// create release
|
||||
console.log(`Creating release ${version}`) |
||||
const r = await octokit.request('POST /repos/{owner}/{repo}/releases', { |
||||
owner: owner, |
||||
repo: repo, |
||||
tag_name: `v${version}`, |
||||
name: `${version}`, |
||||
draft: true, |
||||
headers: headers, |
||||
}) |
||||
release = r.data |
||||
} |
||||
|
||||
|
||||
let ymlFiles = await readReleaseFilesFromLocalDirectory() |
||||
ymlFiles = ymlFiles.filter((file) => file.endsWith('.yml') && file.startsWith('latest')) |
||||
|
||||
console.log(`Found ${ymlFiles.length} yml files to upload`) |
||||
|
||||
// read and parse yml latest files
|
||||
// the yml files contain the sha512 hash and file size of the executable
|
||||
// we need to recalculate the hash and file size of the executable
|
||||
// and update the yml files
|
||||
// this is because the executable is resigned after the yml files are created
|
||||
for (const file of ymlFiles) { |
||||
const content = fs.readFileSync(path.join(__dirname, '../../../release', file), 'utf8') |
||||
const parsed = YAML.parse(content) |
||||
const hashes:{ |
||||
url: string, |
||||
sha512: string, |
||||
size: number |
||||
}[] = [] |
||||
if(parsed.files) { |
||||
console.log(`Found`, parsed.files) |
||||
for (const f of parsed.files) { |
||||
const executable = f.url |
||||
const exists = fs.existsSync(path.join(__dirname, '../../../release', executable)) |
||||
if (!exists) { |
||||
console.log(`File ${executable} does not exist on local fs. Skipping...`) |
||||
continue |
||||
}else{ |
||||
console.log(`File ${executable} exists on local fs. Recalculating hash...`) |
||||
// calculate sha512 hash of executable
|
||||
const hash:string = await hashFile(path.join(__dirname, '../../../release', executable)) |
||||
console.log(hash) |
||||
// calculate file size of executable
|
||||
const stats = fs.statSync(path.join(__dirname, '../../../release', executable)) |
||||
const fileSizeInBytes = stats.size |
||||
console.log(fileSizeInBytes) |
||||
hashes.push({ |
||||
url: executable, |
||||
sha512: hash, |
||||
size: fileSizeInBytes |
||||
}) |
||||
if(parsed.path === executable) { |
||||
parsed.sha512 = hash |
||||
parsed.size = fileSizeInBytes |
||||
} |
||||
} |
||||
} |
||||
} |
||||
console.log(hashes) |
||||
parsed.files = hashes |
||||
const newYml = YAML.stringify(parsed) |
||||
fs.writeFileSync(path.join(__dirname, '../../../release', file), newYml) |
||||
} |
||||
|
||||
let files = await readReleaseFilesFromLocalDirectory() |
||||
|
||||
files = files.filter((file) => file.endsWith('.zip') || file.endsWith('.dmg') || file.endsWith('.exe') || file.endsWith('.AppImage') || file.endsWith('.snap') || file.endsWith('.deb') || file.startsWith('latest')) |
||||
console.log(`Found ${files.length} files to upload`) |
||||
console.log(files) |
||||
if (!release.draft) { |
||||
console.log(`Release ${version} is not a draft. Aborting...`) |
||||
return |
||||
} |
||||
// upload files
|
||||
for (const file of files) { |
||||
// check if it is already uploaded
|
||||
const asset = release.assets.find((a) => a.label === file) |
||||
if (asset) { |
||||
console.log(`Asset ${file} already uploaded... replacing it`) |
||||
// remove it first
|
||||
await removeAsset(asset) |
||||
} |
||||
await uploadReleaseAsset(release, file, path.join(__dirname, '../../../release', file)) |
||||
} |
||||
} |
||||
|
||||
main() |
||||
|
@ -0,0 +1,55 @@ |
||||
import { ElectronPlugin } from '@remixproject/engine-electron' |
||||
|
||||
const profile = { |
||||
displayName: 'appUpdater', |
||||
name: 'appUpdater', |
||||
description: 'appUpdater', |
||||
} |
||||
|
||||
export class appUpdaterPlugin extends ElectronPlugin { |
||||
constructor() { |
||||
console.log('appUpdaterPlugin') |
||||
super(profile) |
||||
} |
||||
|
||||
onActivation(): void { |
||||
this.on('appUpdater', 'askForUpdate', () => { |
||||
console.log('askForUpdate') |
||||
const upgradeModal = { |
||||
id: 'confirmUpdate', |
||||
title: 'An update is available', |
||||
message: `A new version of Remix Desktop is available. Do you want to update?`, |
||||
modalType: 'modal', |
||||
okLabel: 'Yes', |
||||
cancelLabel: 'No', |
||||
okFn: () => { |
||||
this.call('appUpdater', 'download') |
||||
}, |
||||
cancelFn: () => { |
||||
|
||||
}, |
||||
hideFn: () => null |
||||
} |
||||
this.call('notification', 'modal', upgradeModal) |
||||
}) |
||||
this.on('appUpdater', 'downloadReady', () => { |
||||
console.log('downloadReady') |
||||
const upgradeModal = { |
||||
id: 'confirmInstall', |
||||
title: 'An update is ready to install', |
||||
message: `A new version of Remix Desktop is ready to install. Do you want to install it now? This will close Remix Desktop.`, |
||||
modalType: 'modal', |
||||
okLabel: 'Yes', |
||||
cancelLabel: 'No', |
||||
okFn: () => { |
||||
this.call('appUpdater', 'install') |
||||
}, |
||||
cancelFn: () => { |
||||
|
||||
}, |
||||
hideFn: () => null |
||||
} |
||||
this.call('notification', 'modal', upgradeModal) |
||||
}) |
||||
} |
||||
} |
@ -0,0 +1,30 @@ |
||||
const fs = require('fs'); |
||||
|
||||
exports.default = async function afterbuild(context) { |
||||
// do not run when not on macOS or when not on CIRCLECI
|
||||
if (process.platform !== 'darwin' || !process.env.CIRCLE_BRANCH) { |
||||
return; |
||||
} |
||||
|
||||
console.log('AFTER BUILD', context); |
||||
|
||||
const artifactPaths = context.artifactPaths; |
||||
const newDmgs = artifactPaths.filter((dmg) => dmg.endsWith('.dmg')).map((dmg) => dmg); // Removed unnecessary quotes for consistency
|
||||
|
||||
let existingDmgs = []; |
||||
try { |
||||
// Attempt to read the existing dmgs.json file
|
||||
const data = fs.readFileSync('dmgs.json', 'utf8'); |
||||
const parsedData = JSON.parse(data); |
||||
existingDmgs = parsedData.dmgs || []; // Ensure existingDmgs is an array
|
||||
} catch (error) { |
||||
// If there's an error reading the file (e.g., file does not exist), proceed with an empty array
|
||||
console.log('No existing dmgs.json or error reading file, creating new one.'); |
||||
} |
||||
|
||||
// Combine existing and new dmgs, avoiding duplicates
|
||||
const combinedDmgs = [...new Set([...existingDmgs, ...newDmgs])]; |
||||
|
||||
// Write/overwrite the dmgs.json with the combined list of dmgs
|
||||
fs.writeFileSync('dmgs.json', JSON.stringify({ dmgs: combinedDmgs }, null, 2)); |
||||
}; |
@ -0,0 +1,90 @@ |
||||
const { notarize } = require('@electron/notarize') |
||||
const fs = require('fs') |
||||
const { exec } = require('child_process') // Import the exec function
|
||||
exports.default = async function notarizing(context) { |
||||
const { electronPlatformName, appOutDir } = context // Provided by electron-builder
|
||||
|
||||
console.log('NOTARIZING') |
||||
|
||||
if (electronPlatformName !== 'darwin' || !process.env.CIRCLE_BRANCH) { |
||||
return |
||||
} |
||||
|
||||
const appName = context.packager.appInfo.productFilename |
||||
const appPath = `${appOutDir}/${appName}.app` |
||||
|
||||
// Function to promisify the exec command
|
||||
function execShellCommand(cmd) { |
||||
return new Promise((resolve, reject) => { |
||||
exec(cmd, (error, stdout, stderr) => { |
||||
if (error) { |
||||
reject(new Error(`Error: ${error.message}`)); |
||||
return; |
||||
} |
||||
if (stderr) { |
||||
reject(new Error(`Stderr: ${stderr}`)); |
||||
return; |
||||
} |
||||
console.log(`stdout: ${stdout}`); |
||||
resolve(stdout); |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
// Function to check if the app is stapled
|
||||
// Async function to check the stapling status
|
||||
async function checkStapleStatus() { |
||||
try { |
||||
console.log(`xcrun stapler validate "${appPath}"`) |
||||
await execShellCommand(`xcrun stapler validate "${appPath}"`); |
||||
console.log('App is already stapled. No action needed.'); |
||||
return true |
||||
} catch (error) { |
||||
console.log(`App is not stapled: ${error.message}`); |
||||
return false |
||||
} |
||||
} |
||||
|
||||
|
||||
|
||||
|
||||
async function runNotarize() { |
||||
|
||||
console.log('NOTARIZING + ', `xcrun stapler staple "${appPath}"`) |
||||
console.log({ |
||||
appBundleId: 'org.ethereum.remix-ide', // Your app's bundle ID
|
||||
appPath: `${appOutDir}/${appName}.app`, // Path to your .app
|
||||
appleId: process.env.APPLE_ID, // Your Apple ID
|
||||
appleIdPassword: process.env.APPLE_ID_PASSWORD, // App-specific password
|
||||
teamId: process.env.APPLE_TEAM_ID, // Your Apple Developer team ID (optional)
|
||||
}) |
||||
|
||||
try { |
||||
const r = await notarize({ |
||||
appBundleId: 'org.ethereum.remix-ide', // Your app's bundle ID
|
||||
appPath: `${appOutDir}/${appName}.app`, // Path to your .app
|
||||
appleId: process.env.APPLE_ID, // Your Apple ID
|
||||
appleIdPassword: process.env.APPLE_ID_PASSWORD, // App-specific password
|
||||
teamId: process.env.APPLE_TEAM_ID, // Your Apple Developer team ID (optional)
|
||||
}) |
||||
|
||||
console.log(r) |
||||
|
||||
// Stapling the app
|
||||
console.log('STAPLING', `xcrun stapler staple "${appPath}"`) |
||||
|
||||
await execShellCommand(`xcrun stapler staple "${appPath}"`) |
||||
|
||||
} catch (error) { |
||||
console.error('Error during notarization:', error) |
||||
} |
||||
|
||||
} |
||||
|
||||
if(!await checkStapleStatus()){ |
||||
await runNotarize() |
||||
await checkStapleStatus() |
||||
}else{ |
||||
return [] |
||||
} |
||||
} |
@ -0,0 +1,13 @@ |
||||
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> |
||||
<plist version="1.0"> |
||||
<dict> |
||||
<key>com.apple.security.cs.allow-jit</key> |
||||
<true/> |
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key> |
||||
<true/> |
||||
<key>com.apple.security.cs.allow-dyld-environment-variables</key> |
||||
<true/> |
||||
</dict> |
||||
</plist> |
@ -0,0 +1,39 @@ |
||||
#!/bin/bash |
||||
|
||||
# Path to the JSON file containing the DMG paths |
||||
JSON_FILE="dmgs.json" |
||||
|
||||
# Read the DMGs array from the JSON file |
||||
DMG_PATHS=$(jq -r '.dmgs[]' "$JSON_FILE") |
||||
|
||||
echo $DMG_PATHS |
||||
|
||||
xcrun notarytool store-credentials "notarytool-password" \ |
||||
--apple-id ${APPLE_ID} \ |
||||
--team-id ${APPLE_TEAM_ID} \ |
||||
--password ${APPLE_ID_PASSWORD} |
||||
|
||||
# Use jq to parse the DMGs array and read each line |
||||
while IFS= read -r DMG_PATH; do |
||||
# Remove single quotes from the path if present |
||||
DMG_PATH_CLEANED=$(echo $DMG_PATH | tr -d "'") |
||||
|
||||
echo "Submitting $DMG_PATH_CLEANED for notarization..." |
||||
|
||||
# Replace `your-app-specific-args` with the actual arguments for your app |
||||
# Ensure your notarytool command and arguments are correct for your application |
||||
xcrun notarytool submit "$DMG_PATH_CLEANED" --keychain-profile "notarytool-password" --wait |
||||
|
||||
# Check the command's success |
||||
if [ $? -eq 0 ]; then |
||||
echo "Successfully submitted $DMG_PATH_CLEANED for notarization." |
||||
xcrun stapler staple "$DMG_PATH_CLEANED" |
||||
echo "Successfully stapled $DMG_PATH_CLEANED." |
||||
spctl -a -t open -vvv --context context:primary-signature "$DMG_PATH_CLEANED" |
||||
echo "Successfully checked $DMG_PATH_CLEANED." |
||||
else |
||||
echo "Failed to submit $DMG_PATH_CLEANED for notarization." |
||||
fi |
||||
done < <(jq -r '.dmgs[]' "$JSON_FILE") |
||||
|
||||
echo "All DMG submissions completed." |
@ -0,0 +1,121 @@ |
||||
import { ElectronBasePlugin, ElectronBasePluginClient } from "@remixproject/plugin-electron" |
||||
import { Profile } from "@remixproject/plugin-utils" |
||||
import { autoUpdater } from "electron-updater" |
||||
import { app } from 'electron'; |
||||
|
||||
const profile = { |
||||
displayName: 'appUpdater', |
||||
name: 'appUpdater', |
||||
description: 'appUpdater', |
||||
} |
||||
|
||||
export class AppUpdaterPlugin extends ElectronBasePlugin { |
||||
clients: AppUpdaterPluginClient[] = [] |
||||
constructor() { |
||||
console.log('AppUpdaterPlugin') |
||||
super(profile, clientProfile, AppUpdaterPluginClient) |
||||
this.methods = [...super.methods] |
||||
|
||||
autoUpdater.autoDownload = false |
||||
autoUpdater.disableDifferentialDownload = true |
||||
|
||||
autoUpdater.on('checking-for-update', () => { |
||||
console.log('Checking for update...'); |
||||
this.sendToLog('Checking for update...') |
||||
}) |
||||
autoUpdater.on('update-available', (info: any) => { |
||||
console.log('Update available.', info); |
||||
this.sendToLog('Update available.') |
||||
for (const client of this.clients) { |
||||
client.askForUpdate() |
||||
} |
||||
}) |
||||
autoUpdater.on('update-not-available', () => { |
||||
console.log('Update not available.'); |
||||
this.sendToLog('App is already up to date.') |
||||
|
||||
}) |
||||
autoUpdater.on('error', (err) => { |
||||
console.log('Error in auto-updater. ' + err); |
||||
this.sendToLog('Cannot find updates...') |
||||
}) |
||||
autoUpdater.on('download-progress', (progressObj) => { |
||||
let log_message = "Download speed: " + progressObj.bytesPerSecond; |
||||
log_message = log_message + ' - Downloaded ' + progressObj.percent + '%'; |
||||
log_message = log_message + ' (' + progressObj.transferred + "/" + progressObj.total + ')'; |
||||
console.log(log_message); |
||||
this.sendToLog(log_message) |
||||
}) |
||||
autoUpdater.on('update-downloaded', (info) => { |
||||
console.log('Update downloaded'); |
||||
this.sendToLog('Update downloaded') |
||||
this.sendToLog('processing download... please wait...') |
||||
for(const client of this.clients) { |
||||
client.downloadReady() |
||||
} |
||||
}) |
||||
} |
||||
|
||||
async sendToLog(message: string): Promise<void> { |
||||
for (const client of this.clients) { |
||||
client.call('terminal', 'log', { |
||||
type: 'log', |
||||
value: message, |
||||
}) |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
const clientProfile: Profile = { |
||||
name: 'appUpdater', |
||||
displayName: 'appUpdater', |
||||
description: 'appUpdater', |
||||
methods: ['checkForUpdates', 'download', 'install'], |
||||
} |
||||
|
||||
class AppUpdaterPluginClient extends ElectronBasePluginClient { |
||||
constructor(webContentsId: number, profile: Profile) { |
||||
console.log('AppUpdaterPluginClient') |
||||
super(webContentsId, profile) |
||||
} |
||||
|
||||
async onActivation(): Promise<void> { |
||||
console.log('onActivation', 'appUpdaterPluginClient') |
||||
this.onload(async () => { |
||||
console.log('onload', 'appUpdaterPluginClient') |
||||
this.emit('loaded') |
||||
await this.checkForUpdates() |
||||
}) |
||||
} |
||||
|
||||
async askForUpdate(): Promise<void> { |
||||
this.emit('askForUpdate') |
||||
} |
||||
|
||||
async downloadReady(): Promise<void> { |
||||
// we do a wait here to make sure that the download is done, it's a bug in electron-updater
|
||||
setTimeout(() => { |
||||
this.emit('downloadReady') |
||||
} |
||||
, 10000) |
||||
} |
||||
|
||||
async download(): Promise<void> { |
||||
autoUpdater.downloadUpdate() |
||||
} |
||||
|
||||
async install(): Promise<void> { |
||||
autoUpdater.quitAndInstall() |
||||
} |
||||
|
||||
async checkForUpdates(): Promise<void> { |
||||
console.log('checkForUpdates') |
||||
this.call('terminal', 'log', { |
||||
type: 'log', |
||||
value: 'Remix Desktop version: ' + autoUpdater.currentVersion, |
||||
}) |
||||
autoUpdater.checkForUpdates() |
||||
} |
||||
} |
||||
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue