美文网首页
ElectronForge打包、签名、自动更新

ElectronForge打包、签名、自动更新

作者: 我叫Aliya但是被占用了 | 来源:发表于2023-07-17 22:03 被阅读0次

    Electron Forge由官方推荐和维护,结构优美,原本以为用起来会很顺利,没想到还是有坑。

    打包方案:

    • MAC: @electron-forge/maker-dmg DMG 包供用户安装
    • WINDOWS:
      • @electron-forge/maker-squirrel 框架默认,但它安装时不能选安装路径
      • @electron-forge/maker-wix 官方推荐之一,打出 MSI 镜像包。最大的坑就在这里,它的自动更新几乎不可用,issues 也没人回复,白白花费了很多时间
      • @felixrieseberg/electron-forge-maker-nsis 最后还是换回了electron-builder的 NSIS 方案

    以下记录使用的详细配置

    // forge.config.ts
    module.exports = {
      packagerConfig: {
        name: 'APP_NAME',
        // 不加扩展名,MAC 会自动查找 .icns、WIN 使用 .ico
        icon: './icon/icon',
        // 最终包不使用的代码,不要打入 asar
        ignore: [/\.yarn/, /src\/render/],
        appBundleId: `com.xxx.xxx`,
        appCopyright: `Copyright © 2023 ${packageJson.author}`
      },
      ...
    

    packageJson 中的 dependencies 引用也会被打入 asar 中,非主进程使用的包不要放入 dependencies 可以有效减小包大小

    @electron-forge/maker-dmg

    // forge.config.ts
    const RELEASE_APP_DIR = path.join(__dirname, `./out/${APP_NAME}-${process.platform}-${ARCH}/${APP_NAME}.app`)
    
      makers: [
        {
          name: '@electron-forge/maker-dmg',
          config: {
            icon: './icon/icon.icns',
            background: './icon/background.png',
            format: 'ULFO',
            contents: [
              { x: 192, y: 244, type: 'file', path: RELEASE_APP_DIR },
              { x: 448, y: 244, type: 'link', path: '/Applications' },
              { x: 192, y: 700, type: 'position', path: '.background' },
              { x: 292, y: 700, type: 'position', path: '.VolumeIcon.icns' },
              { x: 392, y: 700, type: 'position', path: '.DS_Store' },
              { x: 492, y: 700, type: 'position', path: '.Trashes' }
            ]
          }
        },
    

    @electron-forge/maker-squirrel for exe

    // forge.config.ts
      makers: [
        {
          name: '@electron-forge/maker-squirrel',
          config: {
            authors: packageJson.author,
            description: packageJson.description,
            copyright: packageJson.author,
            iconUrl: 'https://www.xxxx.com/favicon.ico', // http url only
            setupIcon: path.join(__dirname, `icon/icon.ico`),
            skipUpdateIcon: true
            // certificateFile: './cert.pfx',
            // certificatePassword: process.env.CERTIFICATE_PASSWORD
          }
        },
    

    @electron-forge/maker-wix for msi

    // forge.config.ts
      makers: [
        {
          name: '@electron-forge/maker-wix',
          config: {
            language: '2052" Codepage="utf-8',
            cultures: 'zh-CN,en-US',
            icon: path.join(__dirname, `./icon/icon.ico`),
            shortName: 'APP_EN_NAME',
            manufacturer: packageJson.author,
            appUserModelId: `com.xxx.xxx`,
            upgradeCode: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
            features: {
              autoUpdate: true,  // 无法使用
              autoLaunch: false
            },
            ui: {
              template: wixUiTemplate
            }
          }
    
    const wixUiTemplate = wixUiTemplate = `    <UI Id="UserInterface">
          <Property Id="WixUI_Mode" Value="InstallDir" />
    
          <TextStyle Id="WixUI_Font_Normal" FaceName="Tahoma" Size="8" />
          <TextStyle Id="WixUI_Font_Bigger" FaceName="Tahoma" Size="12" />
          <TextStyle Id="WixUI_Font_Title" FaceName="Tahoma" Size="9" Bold="yes" />
    
          <Property Id="DefaultUIFont" Value="WixUI_Font_Normal" />
    
          <DialogRef Id="ErrorDlg" />
          <DialogRef Id="FatalError" />
          <DialogRef Id="FilesInUse" />
          <DialogRef Id="MsiRMFilesInUse" />
          <DialogRef Id="PrepareDlg" />
          <DialogRef Id="ProgressDlg" />
          <DialogRef Id="ResumeDlg" />
          <DialogRef Id="UserExit" />
    
          <Publish Dialog="ExitDialog" Control="Finish" Event="EndDialog" Value="Return" Order="999">1</Publish>
    
          <Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="InstallDirDlg">NOT Installed</Publish>
          <Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg">Installed AND PATCH</Publish>
    
          <Publish Dialog="InstallDirDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg">1</Publish>
          <Publish Dialog="InstallDirDlg" Control="Next" Event="SetTargetPath" Value="[WIXUI_INSTALLDIR]" Order="1">1</Publish>
          <Publish Dialog="InstallDirDlg" Control="Next" Event="DoAction" Value="WixUIValidatePath" Order="2">NOT WIXUI_DONTVALIDATEPATH</Publish>
          <Publish Dialog="InstallDirDlg" Control="Next" Event="SpawnDialog" Value="InvalidDirDlg" Order="3"><![CDATA[NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID<>"1"]]></Publish>
          <Publish Dialog="InstallDirDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Order="4">WIXUI_DONTVALIDATEPATH OR WIXUI_INSTALLDIR_VALID="1"</Publish>
          <Publish Dialog="InstallDirDlg" Control="ChangeFolder" Property="_BrowseProperty" Value="[WIXUI_INSTALLDIR]" Order="1">1</Publish>
          <Publish Dialog="InstallDirDlg" Control="ChangeFolder" Event="SpawnDialog" Value="BrowseDlg" Order="2">1</Publish>
    
          <Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="InstallDirDlg" Order="1">NOT Installed</Publish>
          <Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="MaintenanceTypeDlg" Order="2">Installed AND NOT PATCH</Publish>
          <Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg" Order="2">Installed AND PATCH</Publish>
    
          <Publish Dialog="MaintenanceWelcomeDlg" Control="Next" Event="NewDialog" Value="MaintenanceTypeDlg">1</Publish>
    
          <Publish Dialog="MaintenanceTypeDlg" Control="RepairButton" Event="NewDialog" Value="VerifyReadyDlg">1</Publish>
          <Publish Dialog="MaintenanceTypeDlg" Control="RemoveButton" Event="NewDialog" Value="VerifyReadyDlg">1</Publish>
          <Publish Dialog="MaintenanceTypeDlg" Control="Back" Event="NewDialog" Value="MaintenanceWelcomeDlg">1</Publish>
    
        </UI>
        <Property Id="WIXUI_INSTALLDIR" Value="APPLICATIONROOTDIRECTORY" />
        <UIRef Id="WixUI_Common" />`
    

    @felixrieseberg/electron-forge-maker-nsis for exe

    // forge.config.ts
      makers: [
        {
          name: '@felixrieseberg/electron-forge-maker-nsis',
          config: {
            // codesigning: {
            //   certificateFile?: string;
            //   certificatePassword?: string;
            // },
            updater: {}
          }
    
    // package.json
      "build": {
        "appId": "com.xxx.xxx",
        "productName": "应用名称",
        "nsis": {
          "oneClick": false,
          "allowElevation": true,
          "createDesktopShortcut": true,
          "createStartMenuShortcut": true,
          "allowToChangeInstallationDirectory": true,
          "perMachine": true,
          "deleteAppDataOnUninstall": true,
          "installerIcon": "icon/icon.ico",
          "installerHeaderIcon": "icon/icon.ico",
          "guid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
        },
        "publish": {
          "provider": "generic",
          "url": "",
          "channel": "latest"
        }
      },
    

    MAC 签名与公证

    坑 2:根据官网配置,始终无法成功公证,最后使用codesignxcrun notarytool指令手动签名与公证

    // forge.config.ts
      hooks: {
        preMake: async () => {
          if (process.platform == 'darwin') {
            await makeMacProfile()
            await signMac('/path/to/xxx.app', 'APP_NAME')
          }
        },
        postMake: async () => {
          if (process.platform == 'darwin') {
            await notarizeMac('/path/to/xxx.dmg')
          }
        }
      }
    

    签名

    将证书存入钥匙串参考此文 “代码签名”部分 1、2

    const NEED_SIGN_FW: string[] = [
      'Contents/Frameworks/Electron" "Framework.framework/Versions/A/Libraries/libEGL.dylib',
      'Contents/Frameworks/Electron" "Framework.framework/Versions/A/Libraries/libffmpeg.dylib',
      'Contents/Frameworks/Electron" "Framework.framework/Versions/A/Libraries/libGLESv2.dylib',
      'Contents/Frameworks/Electron" "Framework.framework/Versions/A/Libraries/libvk_swiftshader.dylib',
      'Contents/Frameworks/Squirrel.framework/Versions/A/Resources/ShipIt'
    ]
    const ETM_DIR = path.join(__dirname, `./entitlements.mac.plist`)
    
    function signMac(appPath: string, APP_NAME: string) {
      const caName = 'Developer ID Application: CompanyName (xxxxxxxx)'
      const needSignDirs: string[] = [appPath]
      NEED_SIGN_FW.forEach((subdir) => {
        needSignDirs.push(`${appPath}/${subdir}`)
      })
    
      return execPromise(`security find-identity -v -p codesigning`)
        .then(({ stdout, stderr }) => {
          return stdout.includes(caName)
        })
        .then((hasCertificate) => {
          if (!hasCertificate) throw new Error('钥匙串中没有需要的证书')
          return execPromise(`xattr -cr ${appPath}`)
        })
        .then(() => {
          const signList: Promise<{ stdout: string; stderr: string }>[] = []
          needSignDirs.forEach((dir) => {
            signList.push(
              execPromise(
                `codesign --force --deep --timestamp --options runtime --entitlements ${ETM_DIR} --sign "${caName}" --verbose=2 -v ${dir}`
              )
            )
          })
          return Promise.all(signList)
        })
        .then((resList) => {
          console.info('【签名完成】', resList)
          return resList
        })
      // 查看应用的签名: codesign -d -v -r - /path/to/xxx.app
    }
    
    function execPromise(command: string): Promise<{ stdout: string; stderr: string }> {
      return new Promise((resolve, reject) => {
        exec(command, (err, stdout, stderr) => {
          if (err) return reject(err)
          else resolve({ stdout, stderr })
        })
      })
    }
    
    <!-- entitlements.mac.plist -->
    <?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-unsigned-executable-memory</key>
        <true/>
        <key>com.apple.security.cs.allow-jit</key>
        <true/>
        <key>com.apple.security.cs.disable-library-validation</key>
        <true/>
      </dict>
    </plist>
    

    公证

    export function makeMacProfile() {
      const conf = `--apple-id "${process.env.NOTARY_APP_ID}" --team-id "${process.env.NOTARY_TEAM_ID}" --password "${process.env.NOTARY_PASSWORD}"`
      return execPromise(`xcrun notarytool store-credentials HAHAHA ${conf}`)
    }
    
    function notarizeMac(dmgPath: string) {
      console.info(`【开始公证】: ${dmgPath}`)
    
      return execPromise(`xcrun notarytool submit ${dmgPath} --keychain-profile "HAHAHA" --wait`).then(
        ({ stdout, stderr }) => {
          const res = stdout || stderr
          if (res.includes('Invalid')) {
            console.info('【公证失败】', res)
            // @ts-ignore
            return logNotarytoolErr(res.match(/id: .{36}/)[0]?.substr(4))
          }
          console.info('【公证完成】', stdout, stderr)
          return res
        }
      )
      // xcrun notarytool log "96e8072f-4a0c-443d-b2c3-076b39376817" --keychain-profile "OneApps"
    }
    
    function logNotarytoolErr(id: string) {
      return execPromise(`xcrun notarytool log "${id}" --keychain-profile "OneApps"`).then(({ stdout, stderr }) => {
        const res = stdout || stderr
        console.info('【公证失败原因】', res)
        return Promise.reject(res)
      })
    }
    

    自动更新

    MAC 自更新依赖@electron-forge/maker-zip

    // forge.config.ts
      makers: [
        {
          name: '@electron-forge/maker-zip',
          platforms: ['darwin'],
          config: {
            macUpdateManifestBaseUrl: 'https://XX.oss.aliyuncs.com/apps',
            macUpdateReleaseNotes: '添加了自动更新功能'
          }
        }
    

    打包后出现RELEASES.jsonXXXX-${platform}-${arch}-${version}.zip,后续自动更新依赖此文件。DMG 包供第一次安装使用。

    import { app, autoUpdater, dialog } from 'electron'
    
    autoUpdater.on('error', (message) => {
      console.error('自动更新', message)
    })
    autoUpdater.on('update-available', () => {
      console.info('自动更新 有新版本')
    })
    autoUpdater.on('update-not-available', () => {
      console.info('自动更新 没有新版本')
    })
    autoUpdater.on('update-downloaded', () => {
      console.info('自动更新 新版本下载完成')
      autoUpdater.quitAndInstall()
    })
    autoUpdater.setFeedURL({ url: 'https://XX.oss.aliyuncs.com/apps/RELEASES.json', serverType: 'json' })
    autoUpdater.checkForUpdates()
    

    WIN 自更新依赖electron-updater隶属于 electron-builder

    打包后出现latest.ymlXXX Setup ${version}.exe.blockmapXXX Setup ${version}.exe

    import { autoUpdater as winAutoUpdater } from 'electron-updater'
    
    winAutoUpdater.on('error', (message) => {
      console.error('自动更新', message)
    })
    winAutoUpdater.on('update-available', () => {
      console.info('自动更新 有新版本')
    })
    winAutoUpdater.on('update-not-available', () => {
      console.info('自动更新 没有新版本')
    })
    winAutoUpdater.on('update-downloaded', () => {
      console.info('自动更新 新版本下载完成')
      winAutoUpdater.quitAndInstall()
    })
    winAutoUpdater.setFeedURL('https://XX.oss.aliyuncs.com/apps')
    winAutoUpdater.checkForUpdates()
    

    相关文章

      网友评论

          本文标题:ElectronForge打包、签名、自动更新

          本文链接:https://www.haomeiwen.com/subject/tnogudtx.html