@shockpkg/ria-packager
Version:
Package for creating Adobe AIR packages
875 lines (682 loc) • 21.4 kB
JavaScript
import _defineProperty from "@babel/runtime/helpers/defineProperty";
import { join as pathJoin } from 'path';
import fse from 'fs-extra';
import { PathType } from '@shockpkg/archive-files';
import { IconIcns } from '@shockpkg/icon-encoder';
import { Plist, ValueString, ValueBoolean, ValueArray, ValueDict } from '@shockpkg/plist-dom';
import { pathRelativeBaseMatch, pathRelativeBase } from "../../util.mjs";
import { PackagerBundle } from "../bundle.mjs";
/**
* PackagerBundleMac constructor.
*
* @param path Output path.
*/
export class PackagerBundleMac extends PackagerBundle {
/**
* Create modern application icon file.
* Enables higher resolutions icons and PNG compression.
* Default false uses the legacy formats of the official packager.
*/
/**
* Create modern document type icon file.
* Enables higher resolutions icons and PNG compression.
* Default false uses the legacy formats of the official packager.
*/
/**
* Info.plist file.
*/
/**
* Info.plist data.
*/
/**
* PkgInfo file.
*/
/**
* PkgInfo data.
*/
/**
* Remove unnecessary OS files from older versions of the framework.
* The official packages will include these if they are present in SDK.
*/
/**
* Optionally preserve resource mtime.
* The official packager does not preserve resource mtimes.
*/
/**
* 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.
*/
/**
* 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.
*/
/**
* Remove unnecessary helper files from framework.
* Set to false to match the behavior of SDK versions before 25.0.0.134.
*/
/**
* 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.
*/
/**
* Extension mapping.
*/
constructor(path) {
super(path);
_defineProperty(this, "applicationIconModern", false);
_defineProperty(this, "fileTypeIconModern", false);
_defineProperty(this, "infoPlistFile", null);
_defineProperty(this, "infoPlistData", null);
_defineProperty(this, "pkgInfoFile", null);
_defineProperty(this, "pkgInfoData", null);
_defineProperty(this, "frameworkCleanOsFiles", false);
_defineProperty(this, "preserveResourceMtime", false);
_defineProperty(this, "plistDocumentTypeNameIsDescription", true);
_defineProperty(this, "plistHighResolutionCapable", true);
_defineProperty(this, "frameworkCleanHelpers", true);
_defineProperty(this, "plistHasAppTransportSecurity", true);
_defineProperty(this, "_extensionMapping", new Map());
}
/**
* 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 Info.plist data if any specified, from data or file.
*
* @returns Info.plist data or null.
*/
async getInfoPlistData() {
return this._dataFromValueOrFile(this.infoPlistData, this.infoPlistFile, '\n', 'utf8');
}
/**
* Get Info.plist data as DOM if any specified.
*
* @returns Info.plist DOM or null.
*/
async getInfoPlistDom() {
const data = await this.getInfoPlistData();
if (!data) {
return null;
}
const dom = new Plist();
dom.fromXml(data.toString('utf8'));
return dom;
}
/**
* Get Info.plist data as DOM if any specified, or default.
*
* @returns Info.plist DOM or null.
*/
async getInfoPlistDomOrDefault() {
const dom = await this.getInfoPlistDom();
return dom || new Plist();
}
/**
* Get PkgInfo data if any specified, from data or file.
*
* @returns PkgInfo data or null.
*/
async getPkgInfoData() {
return this._dataFromValueOrFile(this.pkgInfoData, this.pkgInfoFile, null, 'ascii');
}
/**
* Get PkgInfo data if any specified, or default.
*
* @returns PkgInfo data.
*/
async getPkgInfoDataOrDefault() {
const r = await this.getPkgInfoData();
return r || Buffer.from('APPL????', 'ascii');
}
/**
* 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 new ValueString(this._getFilename());
}
/**
* Get plist CFBundleIdentifier value.
*
* @returns The value or null if excluded.
*/
_getPlistCFBundleIdentifier() {
return new ValueString(this._getId());
}
/**
* Get plist CFBundleShortVersionString value.
*
* @returns The value or null if excluded.
*/
_getPlistCFBundleShortVersionString() {
return new ValueString(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 new ValueString(`${versionNumber},${add}`);
}
/**
* Get plist NSHumanReadableCopyright value.
*
* @returns The value or null if excluded.
*/
_getPlistNSHumanReadableCopyright() {
return new ValueString(this._getCopyright() || '');
}
/**
* Get plist CFBundleIconFile value.
*
* @returns The value or null if excluded.
*/
_getPlistCFBundleIconFile() {
const icon = this._getIcon();
return icon && this._uidIcon(icon) ? new ValueString(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 && list.length ? new ValueArray(list.map(s => new ValueString(s))) : null;
}
/**
* Get plist NSHighResolutionCapable value.
*
* @returns The value or null if excluded.
*/
_getPlistNSHighResolutionCapable() {
return this.plistHighResolutionCapable ? new ValueBoolean(this._applicationInfoRequestedDisplayResolution === 'high') : null;
}
/**
* Get plist NSAppTransportSecurity value.
*
* @returns The value or null if excluded.
*/
_getPlistNSAppTransportSecurity() {
return this.plistHasAppTransportSecurity ? new ValueDict(new Map([['NSAllowsArbitraryLoads', new ValueBoolean(true)]])) : null;
}
/**
* Get plist CFBundleDocumentTypes value.
*
* @returns The value or null if excluded.
*/
_getPlistCFBundleDocumentTypes() {
const extensionMapping = this._extensionMapping;
const fileTypes = this._applicationInfoFileTypes;
if (!fileTypes || !fileTypes.size) {
return null;
}
const useDesc = this.plistDocumentTypeNameIsDescription;
const list = [];
for (const [ext, info] of fileTypes) {
const dict = new ValueDict();
const {
value
} = dict;
value.set('CFBundleTypeExtensions', new ValueArray([new ValueString(ext)]));
value.set('CFBundleTypeMIMETypes', new ValueArray([new ValueString(info.contentType)]));
value.set('CFBundleTypeName', new ValueString(useDesc ? info.description || '' : info.name));
value.set('CFBundleTypeRole', new ValueString('Editor'));
const iconFile = extensionMapping.get(ext);
if (iconFile) {
value.set('CFBundleTypeIconFile', new ValueString(iconFile));
}
list.push(dict);
}
return new ValueArray(list);
}
/**
* Get plist CFBundleAllowMixedLocalizations value.
*
* @returns The value or null if excluded.
*/
_getPlistCFBundleAllowMixedLocalizations() {
return new ValueBoolean(true);
}
/**
* Get plist CFBundlePackageType value.
*
* @returns The value or null if excluded.
*/
_getPlistCFBundlePackageType() {
return new ValueString('APPL');
}
/**
* Get plist CFBundleInfoDictionaryVersion value.
*
* @returns The value or null if excluded.
*/
_getPlistCFBundleInfoDictionaryVersion() {
return new ValueString('6.0');
}
/**
* Get plist LSMinimumSystemVersion value.
*
* @returns The value or null if excluded.
*/
_getPlistLSMinimumSystemVersion() {
return new ValueString('10.6');
}
/**
* Get plist LSRequiresCarbon value.
*
* @returns The value or null if excluded.
*/
_getPlistLSRequiresCarbon() {
return new ValueBoolean(true);
}
/**
* Open implementation.
*
* @param applicationData The application descriptor data.
*/
async _open(applicationData) {
this._extensionMapping.clear();
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 = pathJoin(this.path, appBinaryPath);
const appFrameworkPathFull = pathJoin(this.path, appFrameworkPath);
let extractedBinary = false;
let extractedFramework = false; // Extract everything needed from the SDK.
const sdk = await this._openSdk();
await sdk.read(async entry => {
// Ignore any resource forks.
if (entry.type === PathType.RESOURCE_FORK) {
return true;
}
const path = entry.volumePath; // Extract if the binary.
const sdkBinaryPathRel = pathRelativeBase(path, sdkBinaryPath, true);
if (sdkBinaryPathRel !== null) {
const dest = pathJoin(appBinaryPathFull, sdkBinaryPathRel);
await entry.extract(dest);
extractedBinary = true;
return true;
} // Extract if the framework.
const frameworkPathRel = pathRelativeBase(path, sdkFrameworkPath, true);
if (frameworkPathRel !== null) {
// If this is an excluded path, skip over.
if (frameworkExcludes.has(frameworkPathRel.toLowerCase())) {
return null;
}
const dest = pathJoin(appFrameworkPathFull, frameworkPathRel);
await entry.extract(dest);
extractedFramework = true;
return true;
} // Optimization to avoid walking unrelated directories if possible.
return pathRelativeBaseMatch(sdkFrameworkPath, path, true) || 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() {
await this._writeApplicationIcon();
await this._writeFileTypeIcons();
await this._writePkgInfo();
await this._writeInfoPlist();
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 fse.outputFile(dest, data, {
mode
}); // Optionally preserve mtime information.
if (this.preserveResourceMtime) {
const {
mtime
} = options;
if (mtime) {
await fse.utimes(dest, mtime, mtime);
}
}
}
/**
* Get path to a resource file.
*
* @param parts Path parts.
* @returns Full path.
*/
_getResourcePath(...parts) {
return pathJoin(this.path, this.appResourcesPath, ...parts);
}
/**
* Write the application icon if specified.
*/
async _writeApplicationIcon() {
const icon = this._getIcon();
if (!icon || !this._uidIcon(icon)) {
return;
}
const modern = this.applicationIconModern;
const path = pathJoin(this.path, this.appIcnsPath); // Write either a modern or a reference icon.
if (modern) {
// eslint-disable-next-line no-await-in-loop
await this._writeIconModern(path, icon);
} else {
// eslint-disable-next-line no-await-in-loop
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() {
this._extensionMapping.clear();
const mapping = this._extensionMapping;
const fileIcons = this._getFileTypes();
if (!fileIcons) {
return;
}
const modern = this.fileTypeIconModern;
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 = pathJoin(this.path, this._getFileTypeIconPath(name)); // Write either a modern or a reference icon.
if (modern) {
// eslint-disable-next-line no-await-in-loop
await this._writeIconModern(path, icon);
} else {
// eslint-disable-next-line no-await-in-loop
await this._writeIconReference(path, icon);
}
}
}
/**
* Write out PkgInfo file.
*/
async _writePkgInfo() {
const data = await this.getPkgInfoDataOrDefault();
const path = pathJoin(this.path, this.appPkgInfoPath);
await fse.writeFile(path, data);
}
/**
* Generate Info.plist DOM object.
*
* @returns Plist DOM.
*/
async _generateInfoPlist() {
const dom = await this.getInfoPlistDomOrDefault();
const existing = dom.value && dom.value.type === ValueDict.TYPE ? dom.value : null;
const dict = dom.value = new ValueDict(); // A little helper to set values only once.
const done = new Set();
const val = (key, value) => {
if (done.has(key)) {
return;
}
if (value) {
dict.value.set(key, value);
}
done.add(key);
}; // Set all the values in the same order as the official packager.
val('CFBundleAllowMixedLocalizations', this._getPlistCFBundleAllowMixedLocalizations());
val('CFBundlePackageType', this._getPlistCFBundlePackageType());
val('CFBundleInfoDictionaryVersion', this._getPlistCFBundleInfoDictionaryVersion());
val('LSMinimumSystemVersion', this._getPlistLSMinimumSystemVersion());
val('LSRequiresCarbon', this._getPlistLSRequiresCarbon());
val('CFBundleIdentifier', this._getPlistCFBundleIdentifier());
val('CFBundleGetInfoString', this._getPlistCFBundleGetInfoString());
val('CFBundleShortVersionString', this._getPlistCFBundleShortVersionString());
val('NSHumanReadableCopyright', this._getPlistNSHumanReadableCopyright());
val('CFBundleExecutable', this._getPlistCFBundleExecutable());
val('NSAppTransportSecurity', this._getPlistNSAppTransportSecurity());
val('NSHighResolutionCapable', this._getPlistNSHighResolutionCapable());
val('CFBundleIconFile', this._getPlistCFBundleIconFile());
val('CFBundleDocumentTypes', this._getPlistCFBundleDocumentTypes());
val('CFBundleLocalizations', this._getPlistCFBundleLocalizations()); // If any existing values, copy the ones not already set.
if (existing) {
for (const [key, value] of existing.value) {
val(key, value);
}
}
return dom;
}
/**
* Write out Info.plist file.
*/
async _writeInfoPlist() {
const dom = await this._generateInfoPlist();
const path = pathJoin(this.path, this.appInfoPlistPath);
await fse.writeFile(path, dom.toXml({
indentRoot: true,
indentString: ' '
}));
}
/**
* 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 IconIcns();
for (const [path, types] of [[icon.image16x16, ['is32', 's8mk']], [icon.image32x32, ['il32', 'l8mk']], [icon.image48x48, ['ih32', 'h8mk']], [icon.image128x128, ['it32', 't8mk']]]) {
if (!path) {
continue;
} // eslint-disable-next-line no-await-in-loop
const data = await fse.readFile(this._getResourcePath(path));
icns.addFromPng(data, types);
}
await fse.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 IconIcns();
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) {
continue;
} // eslint-disable-next-line no-await-in-loop
const data = await fse.readFile(this._getResourcePath(path));
icns.addFromPng(data, [type]);
}
await fse.writeFile(path, icns.encode());
}
/**
* Get path for a file type icon file.
*
* @param name File name.
* @returns File path.
*/
_getFileTypeIconPath(name) {
return pathJoin('Contents', 'Resources', name);
}
/**
* Get name for a file type icon file.
*
* @param index Unique index.
* @returns File name.
*/
_getFileTypeIconName(index) {
return `DocumentIcon${index}.icns`;
}
}
//# sourceMappingURL=mac.mjs.map