@shockpkg/ria-packager
Version:
Package for creating Adobe AIR packages
803 lines (728 loc) • 22.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.PackagerBundleMac = void 0;
var _promises = require("node:fs/promises");
var _nodePath = require("node:path");
var _archiveFiles = require("@shockpkg/archive-files");
var _iconEncoder = require("@shockpkg/icon-encoder");
var _plistDom = require("@shockpkg/plist-dom");
var _util = require("../../util.js");
var _bundle = require("../bundle.js");
// eslint-disable-next-line jsdoc/require-jsdoc
const asValue = v => v;
// eslint-disable-next-line jsdoc/require-jsdoc
const toValueBoolean = v => new _plistDom.ValueBoolean(v);
// eslint-disable-next-line jsdoc/require-jsdoc
const toValueString = v => new _plistDom.ValueString(v);
/**
* PackagerBundleMac object.
*/
class PackagerBundleMac extends _bundle.PackagerBundle {
/**
* Create modern application icon file.
* Enables higher resolutions icons and PNG compression.
* Default false uses the legacy formats of the official packager.
*/
applicationIconModern = false;
/**
* Create modern document type icon file.
* Enables higher resolutions icons and PNG compression.
* Default false uses the legacy formats of the official packager.
*/
fileTypeIconModern = false;
/**
* Info.plist file.
*/
infoPlistFile = null;
/**
* Info.plist data.
*/
infoPlistData = null;
/**
* PkgInfo file.
*/
pkgInfoFile = null;
/**
* PkgInfo data.
*/
pkgInfoData = null;
/**
* Remove unnecessary OS files from older versions of the framework.
* The official packages will include these if they are present in SDK.
*/
frameworkCleanOsFiles = false;
/**
* Optionally preserve resource mtime.
* The official packager does not preserve resource mtimes.
*/
preserveResourceMtime = false;
/**
* Value of CFBundleDocumentTypes CFBundleTypeName is description, not name.
* Tag value controlled by application descriptor.
* Set to false to match the behavior of SDK versions before 3.2.0.2070.
*/
plistDocumentTypeNameIsDescription = true;
/**
* Add an NSHighResolutionCapable tag to the Info.plist file.
* Tag value controlled by application descriptor.
* Set to false to match the behavior of SDK versions before 3.6.0.6090.
*/
plistHighResolutionCapable = true;
/**
* Remove unnecessary helper files from framework.
* Set to false to match the behavior of SDK versions before 25.0.0.134.
*/
frameworkCleanHelpers = true;
/**
* Add an NSAppTransportSecurity tag to the Info.plist file.
* Tag value controlled by application descriptor.
* Set to false to match the behavior of SDK versions before 27.0.0.128.
*/
plistHasAppTransportSecurity = true;
/**
* Extension mapping.
*/
_extensionMapping = new Map();
/**
* PackagerBundleMac constructor.
*
* @param path Output path.
*/
constructor(path) {
super(path);
}
/**
* If Info.plist is specified.
*
* @returns Is specified.
*/
get hasInfoPlist() {
return !!(this.infoPlistData || this.infoPlistFile);
}
/**
* If PkgInfo is specified.
*
* @returns Is specified.
*/
get hasPkgInfo() {
return !!(this.pkgInfoData || this.pkgInfoFile);
}
/**
* Get app icns file.
*
* @returns File name.
*/
get appIcnsFile() {
return 'Icon.icns';
}
/**
* Get app icns path.
*
* @returns File path.
*/
get appIcnsPath() {
return `Contents/Resources/${this.appIcnsFile}`;
}
/**
* Get app Info.plist path.
*
* @returns File path.
*/
get appInfoPlistPath() {
return 'Contents/Info.plist';
}
/**
* Get app PkgInfo path.
*
* @returns File path.
*/
get appPkgInfoPath() {
return 'Contents/PkgInfo';
}
/**
* Get app resources path.
*
* @returns Resources path.
*/
get appResourcesPath() {
return 'Contents/Resources';
}
/**
* Get app binary path.
*
* @returns Binary path.
*/
getAppBinaryPath() {
return `Contents/MacOS/${this._getFilename()}`;
}
/**
* Get app framework path.
*
* @returns Framework path.
*/
getAppFrameworkPath() {
return 'Contents/Frameworks/Adobe AIR.framework';
}
/**
* Get SDK binary path.
*
* @returns Binary path.
*/
getSdkBinaryPath() {
return 'lib/nai/lib/CaptiveAppEntry';
}
/**
* Get SDK framework path.
*
* @returns Framework path.
*/
getSdkFrameworkPath() {
return 'runtimes/air-captive/mac/Adobe AIR.framework';
}
/**
* Get framework files excluded.
*
* @returns Excluded files in framework.
*/
getFrameworkExcludes() {
const r = [];
if (this.frameworkCleanHelpers) {
// Some files used to create applications, not used after that.
r.push('Versions/1.0/Adobe AIR_64 Helper', 'Versions/1.0/Resources/ExtendedAppEntryTemplate64');
}
if (this.frameworkCleanOsFiles) {
// Some empty junk likely leftover from an Apple ZIP file.
r.push('Versions/1.0/Resources/__MACOSX');
}
return r;
}
/**
* Get PkgInfo data if from data or file, else default.
*
* @returns PkgInfo data.
*/
async getPkgInfoData() {
const {
pkgInfoData,
pkgInfoFile
} = this;
if (typeof pkgInfoData === 'string') {
return new TextEncoder().encode(pkgInfoData);
}
return pkgInfoData || (pkgInfoFile ? (0, _promises.readFile)(pkgInfoFile) : new TextEncoder().encode('APPL????'));
}
/**
* Get file mode value.
*
* @param executable Is the entry executable.
* @returns File mode.
*/
_getFileMode(executable) {
return executable ? 0b111100100 : 0b110100100;
}
/**
* Get plist CFBundleExecutable value.
*
* @returns The value or null if excluded.
*/
_getPlistCFBundleExecutable() {
return this._getFilename();
}
/**
* Get plist CFBundleIdentifier value.
*
* @returns The value or null if excluded.
*/
_getPlistCFBundleIdentifier() {
return this._getId();
}
/**
* Get plist CFBundleShortVersionString value.
*
* @returns The value or null if excluded.
*/
_getPlistCFBundleShortVersionString() {
return this._getVersionNumber();
}
/**
* Get plist CFBundleGetInfoString value.
*
* @returns The value or null if excluded.
*/
_getPlistCFBundleGetInfoString() {
// Strange when no copyright but matches official packager.
const copyright = this._getCopyright();
const versionNumber = this._getVersionNumber();
const add = copyright ? ` ${copyright}` : '';
return `${versionNumber},${add}`;
}
/**
* Get plist NSHumanReadableCopyright value.
*
* @returns The value or null if excluded.
*/
_getPlistNSHumanReadableCopyright() {
return this._getCopyright() || '';
}
/**
* Get plist CFBundleIconFile value.
*
* @returns The value or null if excluded.
*/
_getPlistCFBundleIconFile() {
const icon = this._getIcon();
return icon && this._uidIcon(icon) ? this.appIcnsFile : null;
}
/**
* Get plist CFBundleLocalizations value.
*
* @returns The value or null if excluded.
*/
_getPlistCFBundleLocalizations() {
const langs = this._applicationInfoSupportedLanguages;
const list = langs ? langs.trim().split(/\s+/) : null;
return list?.length ? list : null;
}
/**
* Get plist NSHighResolutionCapable value.
*
* @returns The value or null if excluded.
*/
_getPlistNSHighResolutionCapable() {
return this.plistHighResolutionCapable ? this._applicationInfoRequestedDisplayResolution === 'high' : null;
}
/**
* Get plist NSAppTransportSecurity value.
*
* @returns The value or null if excluded.
*/
_getPlistNSAppTransportSecurity() {
return this.plistHasAppTransportSecurity ? new Map([['NSAllowsArbitraryLoads', true]]) : null;
}
/**
* Get plist CFBundleDocumentTypes value.
*
* @returns The value or null if excluded.
*/
_getPlistCFBundleDocumentTypes() {
const extensionMapping = this._extensionMapping;
const fileTypes = this._applicationInfoFileTypes;
if (!fileTypes?.size) {
return null;
}
const useDesc = this.plistDocumentTypeNameIsDescription;
const list = [];
for (const [ext, info] of fileTypes) {
const map = new Map();
map.set('CFBundleTypeExtensions', [ext]);
map.set('CFBundleTypeMIMETypes', [info.contentType]);
map.set('CFBundleTypeName', useDesc ? info.description || '' : info.name);
map.set('CFBundleTypeRole', 'Editor');
const iconFile = extensionMapping.get(ext);
if (iconFile) {
map.set('CFBundleTypeIconFile', iconFile);
}
list.push(map);
}
return list;
}
/**
* Get plist CFBundleAllowMixedLocalizations value.
*
* @returns The value or null if excluded.
*/
_getPlistCFBundleAllowMixedLocalizations() {
return true;
}
/**
* Get plist CFBundlePackageType value.
*
* @returns The value or null if excluded.
*/
_getPlistCFBundlePackageType() {
return 'APPL';
}
/**
* Get plist CFBundleInfoDictionaryVersion value.
*
* @returns The value or null if excluded.
*/
_getPlistCFBundleInfoDictionaryVersion() {
return '6.0';
}
/**
* Get plist LSMinimumSystemVersion value.
*
* @returns The value or null if excluded.
*/
_getPlistLSMinimumSystemVersion() {
return '10.6';
}
/**
* Get plist LSRequiresCarbon value.
*
* @returns The value or null if excluded.
*/
_getPlistLSRequiresCarbon() {
return true;
}
/**
* Open implementation.
*/
async _open() {
this._extensionMapping.clear();
const {
sdkPath
} = this;
if (!sdkPath) {
throw new Error('SDK path not set');
}
const appBinaryPath = this.getAppBinaryPath();
const appFrameworkPath = this.getAppFrameworkPath();
const sdkBinaryPath = this.getSdkBinaryPath();
const sdkFrameworkPath = this.getSdkFrameworkPath();
const frameworkExcludes = new Set(this.getFrameworkExcludes().map(s => s.toLowerCase()));
const appBinaryPathFull = (0, _nodePath.join)(this.path, appBinaryPath);
const appFrameworkPathFull = (0, _nodePath.join)(this.path, appFrameworkPath);
let extractedBinary = false;
let extractedFramework = false;
// Extract everything needed from the SDK.
const sdk = await (0, _archiveFiles.createArchiveByFileStatOrThrow)(sdkPath, {
nobrowse: this.nobrowse
});
await sdk.read(async entry => {
// Ignore any resource forks.
if (entry.type === _archiveFiles.PathType.RESOURCE_FORK) {
return true;
}
const path = entry.volumePath;
// Extract if the binary.
const sdkBinaryPathRel = (0, _util.pathRelativeBase)(path, sdkBinaryPath, true);
if (sdkBinaryPathRel !== null) {
const dest = (0, _nodePath.join)(appBinaryPathFull, sdkBinaryPathRel);
await entry.extract(dest);
extractedBinary = true;
return true;
}
// Extract if the framework.
const frameworkPathRel = (0, _util.pathRelativeBase)(path, sdkFrameworkPath, true);
if (frameworkPathRel !== null) {
// If this is an excluded path, skip over.
if (frameworkExcludes.has(frameworkPathRel.toLowerCase())) {
return null;
}
const dest = (0, _nodePath.join)(appFrameworkPathFull, frameworkPathRel);
await entry.extract(dest);
extractedFramework = true;
return true;
}
// Optimization to avoid walking unrelated directories if possible.
return (0, _util.pathRelativeBaseMatch)(sdkFrameworkPath, path, true) || (0, _util.pathRelativeBaseMatch)(sdkBinaryPath, path, true) ? true : null;
});
// Check that required components were extracted.
if (!extractedBinary) {
throw new Error(`Failed to locate binary in SDK: ${sdkBinaryPath}`);
}
if (!extractedFramework) {
throw new Error(`Failed to locate framework in SDK: ${sdkFrameworkPath}`);
}
}
/**
* Close implementation.
*/
async _close() {
try {
await Promise.all([this._writeApplicationIcon(), this._writeFileTypeIcons(), this._writePkgInfo()]);
await this._writeInfoPlist();
} finally {
this._extensionMapping.clear();
}
}
/**
* Write resource with data implementation.
*
* @param destination Packaged file relative destination.
* @param data Resource data.
* @param options Resource options.
*/
async _writeResource(destination, data, options) {
// Write resource to file.
const mode = this._getFileMode(options.executable || false);
const dest = this._getResourcePath(destination);
await (0, _promises.mkdir)((0, _nodePath.dirname)(dest), {
recursive: true
});
await (0, _promises.writeFile)(dest, data, {
mode
});
// Optionally preserve mtime information.
if (this.preserveResourceMtime) {
const {
mtime
} = options;
if (mtime) {
await (0, _promises.utimes)(dest, mtime, mtime);
}
}
}
/**
* Get path to a resource file.
*
* @param parts Path parts.
* @returns Full path.
*/
_getResourcePath(...parts) {
return (0, _nodePath.join)(this.path, this.appResourcesPath, ...parts);
}
/**
* Write the application icon if specified.
*/
async _writeApplicationIcon() {
const icon = this._getIcon();
if (!icon || !this._uidIcon(icon)) {
return;
}
const path = (0, _nodePath.join)(this.path, this.appIcnsPath);
// Write either a modern or a reference icon.
// eslint-disable-next-line unicorn/prefer-ternary
if (this.applicationIconModern) {
await this._writeIconModern(path, icon);
} else {
await this._writeIconReference(path, icon);
}
}
/**
* Write file type icons, creating extension name mapping.
* Avoids writting duplicate icons where the file/data is the same.
*/
async _writeFileTypeIcons() {
const mapping = this._extensionMapping;
mapping.clear();
const fileIcons = this._getFileTypes();
if (!fileIcons) {
return;
}
// Write either a modern or a reference icon.
const write = this.fileTypeIconModern ? async (path, icon) => this._writeIconModern(path, icon) : async (path, icon) => this._writeIconReference(path, icon);
const writes = [];
const did = new Map();
let index = 0;
for (const [ext, {
icon
}] of fileIcons) {
if (!icon) {
continue;
}
// Compute a unique identifier for the used icon set paths.
const uid = this._uidIcon(icon);
if (!uid) {
continue;
}
// Check if file was already generated for this icon set.
const done = did.get(uid);
if (done) {
mapping.set(ext, done);
continue;
}
// Compute name for this icon set and cache.
const name = this._getFileTypeIconName(index++);
did.set(uid, name);
mapping.set(ext, name);
// Get the path to write to.
const path = (0, _nodePath.join)(this.path, this._getFileTypeIconPath(name));
writes.push(write(path, icon));
}
await Promise.all(writes);
}
/**
* Write out PkgInfo file.
*/
async _writePkgInfo() {
const data = await this.getPkgInfoData();
const d = typeof data === 'function' ? await data() : data;
const path = (0, _nodePath.join)(this.path, this.appPkgInfoPath);
await (0, _promises.writeFile)(path, d);
}
/**
* Generate Info.plist XML string.
*
* @returns XML string.
*/
async _generateInfoPlist() {
const {
infoPlistData,
infoPlistFile
} = this;
const dom = new _plistDom.Plist();
if (typeof infoPlistData === 'function') {
const d = await infoPlistData();
dom.fromXml(typeof d === 'string' ? d : new TextDecoder().decode(d));
} else if (typeof infoPlistData === 'string') {
dom.fromXml(infoPlistData);
} else if (infoPlistData) {
dom.fromXml(new TextDecoder().decode(infoPlistData));
} else if (infoPlistFile) {
dom.fromXml(await (0, _promises.readFile)(infoPlistFile, 'utf8'));
}
const existing = dom.value && dom.value.type === _plistDom.ValueDict.TYPE ? dom.value : null;
const dict = dom.value = new _plistDom.ValueDict();
const done = new Set();
/**
* A little helper to set values only once.
*
* @param key Key string.
* @param value The value or null.
* @param wrap Wrap value.
*/
const val = (key, value, wrap) => {
if (done.has(key)) {
return;
}
if (value !== null) {
dict.value.set(key, wrap(value));
}
done.add(key);
};
// Set all the values in the same order as the official packager.
val('CFBundleAllowMixedLocalizations', this._getPlistCFBundleAllowMixedLocalizations(), toValueBoolean);
val('CFBundlePackageType', this._getPlistCFBundlePackageType(), toValueString);
val('CFBundleInfoDictionaryVersion', this._getPlistCFBundleInfoDictionaryVersion(), toValueString);
val('LSMinimumSystemVersion', this._getPlistLSMinimumSystemVersion(), toValueString);
val('LSRequiresCarbon', this._getPlistLSRequiresCarbon(), toValueBoolean);
val('CFBundleIdentifier', this._getPlistCFBundleIdentifier(), toValueString);
val('CFBundleGetInfoString', this._getPlistCFBundleGetInfoString(), toValueString);
val('CFBundleShortVersionString', this._getPlistCFBundleShortVersionString(), toValueString);
val('NSHumanReadableCopyright', this._getPlistNSHumanReadableCopyright(), toValueString);
val('CFBundleExecutable', this._getPlistCFBundleExecutable(), toValueString);
val('NSAppTransportSecurity', this._getPlistNSAppTransportSecurity(), v => {
const r = new _plistDom.ValueDict();
for (const [k, d] of v) {
r.set(k, new _plistDom.ValueBoolean(d));
}
return r;
});
val('NSHighResolutionCapable', this._getPlistNSHighResolutionCapable(), toValueBoolean);
val('CFBundleIconFile', this._getPlistCFBundleIconFile(), toValueString);
val('CFBundleDocumentTypes', this._getPlistCFBundleDocumentTypes(), v => {
const r = new _plistDom.ValueArray();
for (const map of v) {
const d = new _plistDom.ValueDict();
for (const [k, t] of map) {
d.set(k, Array.isArray(t) ? new _plistDom.ValueArray(t.map(toValueString)) : new _plistDom.ValueString(t));
}
r.push(d);
}
return r;
});
val('CFBundleLocalizations', this._getPlistCFBundleLocalizations(), v => new _plistDom.ValueArray(v.map(toValueString)));
// If any existing values, copy the ones not already set.
if (existing) {
for (const [key, value] of existing.value) {
val(key, value, asValue);
}
}
return dom.toXml({
indentRoot: true,
indentString: ' '
});
}
/**
* Write out Info.plist file.
*/
async _writeInfoPlist() {
const xml = await this._generateInfoPlist();
const path = (0, _nodePath.join)(this.path, this.appInfoPlistPath);
await (0, _promises.writeFile)(path, xml);
}
/**
* Calculate UID for icon, or null if none of required icons set.
*
* @param icon Icon info.
* @returns UID string or null.
*/
_uidIcon(icon) {
const paths = [icon.image16x16, icon.image32x32, icon.image48x48, icon.image128x128];
// If none set, skip.
let has = false;
for (const p of paths) {
if (p) {
has = true;
break;
}
}
// Compute a unique identifier for the used icon set paths.
return has ? paths.map(s => `${s ? s.length : 0}:${s || ''}`).join('|') : null;
}
/**
* Write icon matching official format.
*
* @param path Icon path.
* @param icon Icon info.
*/
async _writeIconReference(path, icon) {
// Add icons in the same order official packager would use.
const icns = new _iconEncoder.IconIcns();
const readers = [];
for (const [path, types] of [[icon.image16x16, ['is32', 's8mk']], [icon.image32x32, ['il32', 'l8mk']], [icon.image48x48, ['ih32', 'h8mk']], [icon.image128x128, ['it32', 't8mk']]]) {
if (path) {
readers.push(async () => (0, _promises.readFile)(this._getResourcePath(path)).then(d => [d, types]));
}
}
const datas = await Promise.all(readers.map(async f => f()));
for (const [data, types] of datas) {
// eslint-disable-next-line no-await-in-loop
await icns.addFromPng(data, types);
}
await (0, _promises.writeFile)(path, icns.encode());
}
/**
* Write icon using modern format.
*
* @param path Icon path.
* @param icon Icon info.
* @returns Icon written.
*/
async _writeIconModern(path, icon) {
// Add icons in the same order iconutil would.
const icns = new _iconEncoder.IconIcns();
const readers = [];
for (const [path, type] of [
// [icon.image64x64, 'ic12'],
[icon.image128x128, 'ic07'],
// [icon.image256x256, 'ic13'],
// [icon.image256x256, 'ic08'],
[icon.image16x16, 'ic04'], [icon.image512x512, 'ic14'], [icon.image512x512, 'ic09'], [icon.image32x32, 'ic05'], [icon.image1024x1024, 'ic10'], [icon.image32x32, 'ic11']]) {
if (path) {
readers.push(async () => (0, _promises.readFile)(this._getResourcePath(path)).then(d => [d, type]));
}
}
const datas = await Promise.all(readers.map(async f => f()));
for (const [data, type] of datas) {
// eslint-disable-next-line no-await-in-loop
await icns.addFromPng(data, [type]);
}
await (0, _promises.writeFile)(path, icns.encode());
}
/**
* Get path for a file type icon file.
*
* @param name File name.
* @returns File path.
*/
_getFileTypeIconPath(name) {
return (0, _nodePath.join)('Contents', 'Resources', name);
}
/**
* Get name for a file type icon file.
*
* @param index Unique index.
* @returns File name.
*/
_getFileTypeIconName(index) {
return `DocumentIcon${index}.icns`;
}
}
exports.PackagerBundleMac = PackagerBundleMac;
//# sourceMappingURL=mac.js.map