';

Adobe CEP 扩展开发教程 「 4 」签名与打包

签名并打包发布你的扩展

CEP 扩展必须有签名才能运行,而所谓签名是验证扩展文件是否与签名时一致的手段,能保证你的扩展不被篡改和识别扩展作者。所以你会发现修改了别人的扩展插件后,扩展就无法运行了。
不过在开发者模式下,  宿主应用(如 PhotoShop)会无视签名,关于打开发者模式,在 「 1 」Hello World! 一文中介绍了。

签名

签名分为和自签名证书(self-signed certificates)或者商业签名证书(commercial certificates),
商业签名可以(也仅可以)在下列数字签名提供商中购买:

商业证书在使用 Adobe Extension Manager 安装时不会有如下警告:

自签名证书警告

不过在 CC 2015 之后 Adobe Extension Manager 已经被移除了(Adobe 现在想让用户都从它的 Adobe Add-Ons 市场上购买、下载插件)

不用付钱我们也可以使用自签名证书,自签名证书可以使用 ZXPSignCmd 创建。

ZXPSignCmd

ZXPSignCmd 是 Adobe 官方发布的签名与打包的命令行工具,有 Windows 和 OSX 2 个平台的版本。这里先介绍使用 ZXPSignCmd 创建证书和打包的方法,如果觉得命令行工具麻烦,可以使用我制作的 GUI 版本,后面会介绍。

ZXPSignCmd

创建证书

ZXPSignCmd -selfSignedCert <countryCode> <stateOrProvince> <organization> <commonName> <password> <outputPath.p12>
ZXPSignCmd -selfSignedCert <国家代码> <地区> <组织名> <证书所有者名称> <证书密码> <证书名.p12>

例子:

ZXPSignCmd -selfSignedCert CN Changsha nullice.com nullice 123456 我的证书.p12>

签名并打包

ZXPSignCmd -sign <inputDirectory> <outputZxp> <p12> <p12Password> -tsa <timestampURL>
ZXPSignCmd -sign <要打包的项目目录> <输出文件路径> <证书路径> <证书密码> -tsa <时间戳服务地址>

其中 -tsa <时间戳服务地址> 不需要可以省略。

ZXPSignCmd  -sign  "PS.fonTags\fonTags"  "PS.fonTags\我的扩展.zxp"  "我的证书.p12"  "123456"

要注意的是这里输出文件路径如果已经存在了一个文件的话(比如曾经打包的),ZXPSignCmd 是不会自己覆盖它的,需要自己手动删除。

打包后输出的文件是 ZIP 格式的,可以用 ZIP 解压缩工具解压。

ZXP WinGUI

在 Windows 下除了直接使用 ZXPSignCmd ,还可以使用有图形界面的 ZXP WinGUI,注意这不是 Adobe 官方的,只是我自己制作的。是否使用请谨慎判断(开发用的工具能从官方渠道获取的就尽量用官方的,这不仅仅是为了自己的安全也是为了你开发软件的用户安全负责,CEP 的能调用的本地接口很多,如果被置入恶意代码的话很危险,出现像 XcodeGhost 一样的事件就不好了)。

ZXP WinGUI 实际只是直接调用 ZXPSignCmd ,不过除了图形界面以为还有这些方便使用的功能:

  • 自动清除过期的生成文件(覆盖)
  • 拖放文件夹输入项目目录
  • 生成打包 ZXP 的批处理

其中生成打包 ZXP 的批处理,可以在填写配置后生成一个 .bat 批处理文件,以后执行这个批处理就可以打包了。

批处理

 

创建证书

签名并打包

另外如果你喜欢用 gulp 的话,可以看看这篇文章: Automate ZXP Packaging with Gulp.js

 

修改与汉化

打包后插件目录中文件就不可以修改或者删减了,否则都会使签名验证失败,无法载入。

这意味着你的扩展不能在插件目录中存储用户数据或者下载内容。

要存储这些扩展运行中产生的数据,请存储到类似 cs.getSystemPath(SystemPath.USER_DATA) 的系统目录中去,总之就是不要让你的扩展在扩展目录中存储数据,或者修改自己的文件。

对于汉化扩展,在修改之后扩展中的文件后,需要删除扩展目录中的 META-INF 文件夹,并重新签名。

安装扩展

过于有 Adobe Extension Manager  可以来安装和管理扩展,不过它已经不会在 CC 2015 以后的版本上了,Adobe 已经停止了对这个工具的支持(Announcement: Extension Manager End of Life Notification

Extension Manager 已死

Adobe 想让人们都去它的 Adobe Add-Ons 市场下载扩展,不过实际上 Adobe Add-Ons 并不好用,尤其是国内网络环境下,它需要 Adobe Creative Cloud 客户端安装扩展不仅速度慢而且很容易失败。

Adobe Add-Ons

所以目前的扩展主流是自己发布文件让

  • 用户自己复制文件到扩展安装目录
  • 用户执行 .JSX、.Bat 脚本,帮助用户一键复制文件到扩展安装目录
  • 自己制作一个安装器软件
  • Adobe Extension Manager 的开源替代品 :ZXPInstaller

其中 ZXPInstaller 是一个功能和 Adobe Extension Manager 差不多的软件,不过 40 多 MB 的体积真不想跟用户说下个几 MB 的扩展前先装个这家伙…

比较好的方法是使用一个类似下面这样的 JSX 脚本文件来安装扩展:

(function() {
  var Installer, e;
  $.extensionApp = {
    COMPANY_NAME: 'nullice',
    CONTACT_INFO: 'ui@nullice.com',
    PRODUCT_NAME: 'fonTags',
    PRODUCT_ID: 'fonTags',
    PRODUCT_VERSION: '1.0',
    MIN_VERSION: 1,
    MAX_VERSION: 99,
    RELATIVE_SRC_PATH: 'com.nullice.pschen.fonTags'
  }

  Installer = (function() {
    Installer.prototype.logString = '';

    Installer.prototype.photoshopVersions = {
      10: "CS3",
      11: "CS4",
      12: "CS5",
      13: "CS6",
      14: "CC",
      15: "CC 2014",
      16: "CC 2015"
    };

    Installer.prototype.isWindows = function() {
      return $.os.match(/windows/i);
    };

    Installer.prototype.isMac = function() {
      return !this.isWindows();
    };

    function Installer(config) {
      this.config = config;
      this.configure();
      this.preflight();
      this.copyFiles();
      this.teardown();
    }

    Installer.prototype.configure = function(config) {
      var k, v, _ref;
      if (this.config == null) {
        throw Error("未定义配置");
      }
      _ref = this.config;
      for (k in _ref) {
        v = _ref[k];
        this[k] = v;
      }
      this.CURRENT_PATH = File($.fileName).path;
      this.LOG_FILE_POINTER = this.createNewLogFile();
      this.CURRENT_PS_VERSION = parseInt(app.version.split('.')[0]);
      this.CEP_FOLDER = 'CEP';
      if (this.CURRENT_PS_VERSION === 14) {
        this.CEP_FOLDER = 'CEPServiceManager4';
      }
      this.SYSTEM_PATH = "" + Folder.commonFiles + "/Adobe/" + this.CEP_FOLDER + "/extensions";
      this.LOCAL_PATH = "" + Folder.userData + "/Adobe/" + this.CEP_FOLDER + "/extensions";
      this.SYSTEM_POINTER = Folder("" + this.SYSTEM_PATH + "/" + this.PRODUCT_NAME);
      this.LOCAL_POINTER = Folder("" + this.LOCAL_PATH + "/" + this.PRODUCT_NAME);
      this.SRC_POINTER = Folder("" + this.CURRENT_PATH + "/" + this.RELATIVE_SRC_PATH);
      return this.log("Product: " + this.PRODUCT_NAME + "\nVersion: " + this.PRODUCT_VERSION + "\nPhotoshop version: " + this.photoshopVersions[this.CURRENT_PS_VERSION] + "\nOperating system: " + $.os + "\nLocale: " + $.locale + "\nInstallation source: " + this.CURRENT_PATH);
    };

    Installer.prototype.preflight = function() {
      if (this.CURRENT_PS_VERSION < this.MIN_VERSION) {
        this.error("安装失败. " + this.PRODUCT_NAME + " 需要      " + this.photoshopVersions[this.MIN_VERSION] + " 或更新的版本.    ");
      }
      if (this.CURRENT_PS_VERSION > this.MAX_VERSION) {
        this.error("安装失败. " + this.PRODUCT_NAME + " 仅支持      " + this.photoshopVersions[this.MAX_VERSION] + "版本.    ");
      }
      if (this.SYSTEM_POINTER.exists) {
        this.rm(this.SYSTEM_POINTER);
      }
      if (this.LOCAL_POINTER.exists) {
        return this.rm(this.LOCAL_POINTER);
      }
    };

    Installer.prototype.teardown = function() {
      alert("安装完成\n\n请重启应用程序以使用 " + this.PRODUCT_NAME + ".");
      return this.log("安装完成");
    };

    Installer.prototype.rm = function(obj) {
      var file, path, _i, _len, _ref;
      if (obj instanceof File || obj.getFiles().length === 0) {
        path = obj.fsName;
        if (obj.remove()) {
          return this.log("rm " + path);
        }
        this.error("失败: rm " + path + " (" + obj.error + ")");
      }
      _ref = obj.getFiles().reverse();
      for (_i = 0, _len = _ref.length; _i < _len; _i++) {
        file = _ref[_i];
        arguments.callee.call(this, file);
      }
      arguments.callee.call(this, obj);
      return true;
    };

    Installer.prototype.cp = function(src, dest) {
      var file, isFile, newDest, path, _i, _len, _ref;
      if (src instanceof File) {
        if (src.copy(dest)) {
          return this.log("cp " + src.fsName + " -> " + dest.fsName);
        }
        this.error("错误: cp " + src.fsName + " -> " + dest.fsName + " (" + src.error + ")");
      }
      if (!dest.create()) {
        this.error("创建失败: " + dest.fsName);
      }
      _ref = src.getFiles();
      for (_i = 0, _len = _ref.length; _i < _len; _i++) {
        file = _ref[_i];
        isFile = file instanceof File;
        path = "" + (encodeURI(dest.fsName)) + "/" + file.name;
        newDest = isFile ? File(path) : Folder(path);
        arguments.callee.apply(this, [file, newDest]);
      }
      return true;
    };

    Installer.prototype.error = function(msg) {
      this.log(msg);
      throw Error(msg);
    };

    Installer.prototype.copyFiles = function() {
      return this.cp(this.SRC_POINTER, this.LOCAL_POINTER);
    };

    Installer.prototype.log = function(msg) {
      var file;
      file = this.LOG_FILE_POINTER;
      if (!file.open('e')) {
        throw Error("无法打开日志文件");
      }
      file.seek(0, 2);
      if (!file.writeln(msg)) {
        throw Error("无法创建日志文件");
      }
      return true;
    };

    Installer.prototype.createNewLogFile = function() {
      var file;
      file = new File("" + this.CURRENT_PATH + "/" + this.PRODUCT_NAME + ".log");
      if (!file.open('w')) {
        throw Error("无法创建日志文件");
      }
      if (this.isMac()) {
        file.lineFeed = 'unix';
      }
      file.encoding = "UTF8";
      return file;
    };

    return Installer;

  })();

  try {
    new Installer($.extensionApp);
  } catch (_error) {
    e = _error;
    alert("安装失败:\n" + e + "\n\n可以查看脚本所在文件夹中的安装日志.");
  }

}).call(this);