Electron 应用构建支持统信 UOS 系统安装使用的方法

目录
[隐藏]

Electron 类项目通过 electron-builder 中的 linux 平台下的 deb 配置可一键打包出 deb 安装包。此类包可以在 UOS 系统中安装(一般会安装到 /opt 目录下),但点击快捷键无反应,通过命令行形式运行则提示 sandbox 相关错误。

1 关于 sandbox 异常的解决方法

一种简单的解决办法是修改快捷键文件,添加 --no-sandbox 启动参数。
假若文件位置为 /usr/share/applications/me.lzw.app.desktop,编辑它,并在 Exec 行后添加 --no-sandbox,示例:

Exec=/opt/me.lzw.app/me.lzw.app --no-sandbox

2 Electron 应用构建支持 no-sandbox 启动参数

通过 electron-builder 构建的应用,可以通过设置 executableArgs 参数来添加启动参数,示例配置:

{
    "linux": {
        "maintainer": "[email protected]",
        "executableArgs": ["--no-sandbox"],
        "target": ["AppImage", "deb"],
        "icon": "build/linux/icons/",
        "category": "Development",
        "extraResources": ["./config/**/*"],
        "desktop": {
            "Icon": "./build/icons/256x256.png",
            "StartupNotify": "false",
            "Encoding": "UTF-8",
            "MimeType": "x-scheme-handler/deeplink"
        }
        // "postInstallscript": "build/linux/postinst.sh",
    }
}

注意:如果是比较低版本的 electronelectron-builder,可能不支持 executableArgs 参数配置。可参考下文方法,构建一个符合 UOS 应用程序规范的 deb 安装包。

3 二进制应用构建为支持 UOS 应用程序规范的安装包

通过 electron-builder 构建出来的 deb 包和统信 UOS 应用程序规范有所差异,如果是希望上架其应用商店,还需按其应用程序规范组织目录结构。规范参考:

通过 electron-updater 打包后,在 release 目录会有一个 linux-unpackedlinux-arm64-unpacked 目录,这里包含了应用程序运行所需的所有文件。
参考如上规范,我们创建如下目录结构:

  • uos-tmp 临时目录
    • opt/apps/me.lzw.app 应用程序安装位置
    • opt/apps/me.lzw.app/info 应用程序信息文件
    • opt/apps/me.lzw.app/files 应用程序文件,将 linux-unpacked 目录下的内容拷贝过来
    • opt/apps/me.lzw.app/entries/applications/me.lzw.app.desktop 快捷方式,安装后会映射到 /usr/share/applications/me.lzw.app.desktop,然后就可以在启动器中看到该应用了
    • DEBIAN debian 包结构
      • control 包的基本信息
      • install 安装脚本

基于如上目录规范,我基于 Node.js 写了一个脚本,用于一键打包,参考:

const { resolve } = require('node:path');
const { writeFileSync } = require('node:fs');
const { execSync } = require('node:child_process');
const { mkdirp, rmrf } = require('@lzwme/fe-utils');
const { env, rootDir, argv } = require('./helper');

async function packForUOS(arch = argv.arch || 'arm64') {
  const epkg = require('../../dist/electron/app/package.json');
  const baseDir = resolve(rootDir, 'release');
  const appId = `lzw.me.app${env}`;
  const version = epkg.version;
  const uosPackTmpDir = resolve(baseDir, `${appId}-${version}`);
  const appDest = resolve(uosPackTmpDir, `opt/apps/${appId}`);
  const architecture = arch === 'x64' ? 'amd64' : arch === 'x86' ? 'i386' : arch;

  rmrf(resolve(baseDir, appId));
  mkdirp(resolve(appDest, 'entries/applications'));
  mkdirp(resolve(appDest, 'files'));
  execSync(`cp -R linux${arch === 'x64' ? '' : arch === 'x86' ? '-ia32' : `-${arch}`}-unpacked/* ${appDest}/files`, { stdio: 'inherit', cwd: baseDir });
  execSync(`cp -R ../dist/electron/builder/res/256x256.png ${appDest}/files/icon.png`, {
    stdio: 'inherit',
    cwd: baseDir,
  });

  // 生成 info 文件
  const info = {
    appid: appId,
    name: epkg.name,
    version: version,
    arch: architecture === 'all' ? ['amd64', 'arm64'] : [architecture],
    permissions: {
      autostart: false,
      notification: true,
      trayicon: true,
      clipboard: true,
      account: true,
      bluetooth: true,
      camera: true,
      audio_record: true,
      installed_apps: true,
      network: true,
      filesystem: true,
    },
  };
  writeFileSync(resolve(appDest, 'info'), JSON.stringify(info, null, 4), 'utf8');

  // 生成 desktop 文件
  const desktopFile = resolve(appDest, `entries/applications/${appId}.desktop`);
  const desktopContent = [
    `[Desktop Entry]`,
    `Name=${info.name}`,
    `Name[zh-CN]=${epkg.appName}`,
    `Comment=${epkg.description}`,
    `Exec=/opt/apps/${appId}/files/lzwme-${env} --no-sandbox --enable-gpu-rasterization --disable-gpu-sandbox %U`,
    `Icon=/opt/apps/${appId}/files/icon.png`,
    `Categories=Utility;`,
    `Terminal=false`,
    `Type=Application`,
    `StartupNotify=true`,
    `Encoding=UTF-8`,
  ].join('\n');
  writeFileSync(desktopFile, desktopContent, 'utf8');

  // 生成 debian 目录及文件
  const debianDir = resolve(uosPackTmpDir, `DEBIAN`);
  mkdirp(debianDir);
  const debianControl = [
    `Source: ${appId}`,
    `Section: unknown`,
    `Priority: optional`,
    `Maintainer: lzw.me <[email protected]>`,
    `Build-Depends: debhelper (>= 11)`,
    `Standards-Version: 4.1.3`,
    `Homepage: https://lzw.me`,
    `Package: ${appId}`,
    `Version: ${version}`,
    `Architecture: ${architecture}`,
    `Depends: libgtk-3-0, libnotify4, libnss3, libxss1, libxtst6, xdg-utils, libatspi2.0-0, libdrm2, libgbm1, libasound2`,
    `Description: ${epkg.appName}`,
    `    ${epkg.description}\n`,
  ].join('\n');
  writeFileSync(resolve(debianDir, 'control'), debianControl, 'utf8');
  // 生成 install 文件
  writeFileSync(resolve(debianDir, 'install'), `opts/apps/${appId}/ /opt/apps`, 'utf8');

  execSync(`dpkg-deb -b ${uosPackTmpDir}/ lzwme-${env}-uos-${arch}-${info.version}.deb`, { stdio: 'inherit', cwd: baseDir });
}

if (require.main === module) packForUOS(argv.arch);
module.exports = packForUOS;

electron-builder 构建完成后,继续执行该脚本,即可生成所需符合 uos 应用程序规范的 deb 安装包。

4 相关参考

  • https://uosdn.uniontech.com/#document3?dirid=656ef27dbd766615b0b0300e&id=65702eaebd766615b0b0310d
  • https://blog.csdn.net/weixin_45813250/article/details/117027334
  • https://blog.csdn.net/weixin_43855876/article/details/107251980
  • https://blog.csdn.net/Karim_Benzema/article/details/136351345
  • https://www.bilibili.com/opus/503101456761048630