mirror of
https://github.com/cappuccino/cappuccino.git
synced 2026-08-28 22:47:04 +00:00
Previously, when popen was called, the 3 streams (stdin, stderr and stdourt) where never closed. This was the reason of the having an impossible number of open files. This patch ensure all streams are closed after using them. This means that the ulimit trick is no more necessary
406 lines
15 KiB
Plaintext
Executable File
406 lines
15 KiB
Plaintext
Executable File
#!/usr/bin/env objj
|
|
|
|
require("narwhal").ensureEngine("rhino");
|
|
|
|
@import <Foundation/Foundation.j>
|
|
|
|
@import "../lib/cappuccino/objj-analysis-tools.j"
|
|
@import "../lib/cappuccino/cib-analysis-tools.j"
|
|
|
|
var FILE = require("file");
|
|
var OS = require("os");
|
|
|
|
var CACHEMANIFEST = require("objective-j/cache-manifest");
|
|
|
|
var stream = require("narwhal/term").stream;
|
|
var parser = new (require("narwhal/args").Parser)();
|
|
|
|
parser.usage("INPUT_PROJECT OUTPUT_PROJECT");
|
|
parser.help("Analyze and strip unused files from a Cappuccino project's .sj bundles.");
|
|
|
|
parser.option("-m", "--main", "main")
|
|
.def("main.j")
|
|
.set()
|
|
.help("The relative path (from INPUT_PROJECT) to the main file (default: 'main.j')");
|
|
|
|
parser.option("-F", "--framework", "frameworks")
|
|
.def(["Frameworks"])
|
|
.push()
|
|
.help("Add a frameworks directory, relative to INPUT_PROJECT (default: ['Frameworks'])");
|
|
|
|
parser.option("-f", "--force", "force")
|
|
.def(false)
|
|
.set(true)
|
|
.help("Force overwriting OUTPUT_PROJECT if it exists");
|
|
|
|
parser.option("--index", "index")
|
|
.def("index.html")
|
|
.set()
|
|
.help("The root HTML file to modify (default: index.html) (NOTE: currently only used by '--manifest' option)");
|
|
|
|
parser.option("-p", "--pngcrush", "png")
|
|
.def(false)
|
|
.set(true)
|
|
.help("Run pngcrush on all PNGs (pngcrush must be installed!)");
|
|
|
|
parser.option("--manifest", "manifest")
|
|
.set(true)
|
|
.help("Generate HTML5 cache manifest.");
|
|
|
|
parser.option("-v", "--verbose", "verbose")
|
|
.def(false)
|
|
.set(true)
|
|
.help("Verbose logging");
|
|
|
|
parser.helpful();
|
|
|
|
function main(args)
|
|
{
|
|
var options = parser.parse(args);
|
|
|
|
if (options.args.length < 2) {
|
|
parser.printUsage(options);
|
|
return;
|
|
}
|
|
|
|
CPLogRegister(CPLogPrint);
|
|
|
|
// HACK: ensure trailing slashes for "relative" to work correctly
|
|
var rootPath = FILE.path(options.args[0]).absolute().join("");
|
|
var outputPath = FILE.path(options.args[1]).absolute().join("");
|
|
|
|
if (outputPath.exists()) {
|
|
if (options.force) {
|
|
// FIXME: why doesn't this work?!
|
|
//outputPath.rmtree();
|
|
OS.system(["rm", "-rf", outputPath]);
|
|
} else {
|
|
stream.print("\0red(OUTPUT_PROJECT " + outputPath + " exists. Use -f to overwrite.\0)");
|
|
OS.exit(1);
|
|
}
|
|
}
|
|
|
|
press(rootPath, outputPath, options);
|
|
}
|
|
|
|
function press(rootPath, outputPath, options) {
|
|
stream.print("\0yellow("+Array(81).join("=")+"\0)");
|
|
stream.print("Application root: \0green(" + rootPath + "\0)");
|
|
stream.print("Output directory: \0green(" + outputPath + "\0)");
|
|
|
|
var outputFiles = {},
|
|
totalBytes = {executable:0, data:0, mhtml:0};
|
|
|
|
var environment = "Browser";
|
|
// analyze and gather files for each environment:
|
|
var returnedBytes = pressEnvironment(rootPath, outputFiles, environment, options);
|
|
|
|
for (var i in returnedBytes)
|
|
totalBytes[i] += returnedBytes[i];
|
|
|
|
// phase 4: copy everything and write out the new files
|
|
stream.print("\0red(PHASE 4:\0) copy to output \0green(" + rootPath + "\0) => \0green(" + outputPath + "\0)");
|
|
|
|
FILE.copyTree(rootPath, outputPath);
|
|
|
|
for (var path in outputFiles) {
|
|
var file = outputPath.join(rootPath.relative(path));
|
|
|
|
var parent = file.dirname();
|
|
if (!parent.exists()) {
|
|
CPLog.warn(parent + " doesn't exist, creating directories.");
|
|
parent.mkdirs();
|
|
}
|
|
|
|
if (typeof outputFiles[path] !== "string")
|
|
outputFiles[path] = outputFiles[path].join("");
|
|
|
|
stream.print((file.exists() ? "\0red(Overwriting:\0) " : "\0green(Writing:\0) ") + file);
|
|
|
|
FILE.write(file, outputFiles[path], { charset : "UTF-8" });
|
|
}
|
|
|
|
// phase 4a: update the info.plist to contain meta information about the app
|
|
stream.print("\0red(PHASE 4a:\0) Add application size data to Info.plist");
|
|
|
|
var outputInfoPlistPath = FILE.join(outputPath, "Info.plist"),
|
|
outputInfoPlistContents = FILE.read(outputInfoPlistPath, { charset:"UTF-8" }),
|
|
format = CFPropertyList.sniffedFormatOfString(outputInfoPlistContents),
|
|
outputInfoPlist = CFPropertyList.propertyListFromString(outputInfoPlistContents);
|
|
|
|
// read in the default theme name, and attempt to get its size
|
|
var themeName = outputInfoPlist.valueForKey("CPDefaultTheme") || "Aristo2",
|
|
themePath = nil;
|
|
|
|
if (themeName === "Aristo" || themeName === "Aristo2")
|
|
themePath = FILE.join(outputPath, options.frameworks, "AppKit", "Resources", themeName+".blend");
|
|
else
|
|
themePath = FILE.join(outputPath, "Resources", themeName + ".blend");
|
|
|
|
if (FILE.exists(themePath))
|
|
{
|
|
var themeEnvPath = FILE.join(themePath, environment + ".environment"),
|
|
themeExecutablePath = FILE.join(themeEnvPath, themeName+".blend.sj"),
|
|
themeDataPath = FILE.join(themeEnvPath, "dataURLs.txt"),
|
|
themeMHTMLPath = FILE.join(themeEnvPath, "MHTMLPaths.txt");
|
|
|
|
totalBytes.executable += FILE.read(themeExecutablePath, {charset : "UTF-8"}).length;
|
|
totalBytes.data += FILE.read(themeDataPath, {charset : "UTF-8"}).length;
|
|
totalBytes.mhtml += FILE.read(themeMHTMLPath, {charset : "UTF-8"}).length;
|
|
}
|
|
|
|
outputInfoPlist.setValueForKey("CPApplicationSize", [CPDictionary dictionaryWithJSObject:totalBytes]);
|
|
|
|
FILE.write(outputInfoPlistPath, CFPropertyList.stringFromPropertyList(outputInfoPlist, format), { charset:"UTF-8" });
|
|
|
|
// strip known unnecessary files
|
|
// outputPath.glob("**/Frameworks/Debug").forEach(function(debugFramework) {
|
|
// outputPath.join(debugFramework).rmtree();
|
|
// });
|
|
// outputPath.join("index-debug.html").remove();
|
|
|
|
if (options.png) {
|
|
pngcrushDirectory(outputPath);
|
|
}
|
|
|
|
if (options.manifest) {
|
|
CACHEMANIFEST.generateManifest(outputPath, {
|
|
index : outputPath.join(options.index)
|
|
});
|
|
}
|
|
}
|
|
|
|
function pressEnvironment(rootPath, outputFiles, environment, options) {
|
|
|
|
var mainPath = String(rootPath.join(options.main));
|
|
var frameworks = options.frameworks.map(function(framework) { return rootPath.join(framework); });
|
|
|
|
stream.print("\0yellow("+Array(81).join("=")+"\0)");
|
|
stream.print("Main file: \0green(" + mainPath + "\0)");
|
|
stream.print("Frameworks: \0green(" + frameworks + "\0)");
|
|
stream.print("Environment: \0green(" + environment + "\0)");
|
|
|
|
var analyzer = new ObjectiveJRuntimeAnalyzer(rootPath);
|
|
|
|
var _OBJJ = analyzer.require("objective-j");
|
|
|
|
analyzer.setIncludePaths(frameworks);
|
|
analyzer.setEnvironments([environment, "ObjJ"]);
|
|
|
|
// build list of cibs to inspect for dependent classes
|
|
// FIXME: what's the best way to determine which cibs to look in?
|
|
var cibs = FILE.glob(rootPath.join("**", "*.cib")).filter(function(path) { return !(/Frameworks/).test(path); });
|
|
|
|
// phase 1: get global defines
|
|
stream.print("\0red(PHASE 1:\0) Loading application...");
|
|
|
|
analyzer.initializeGlobalRecorder();
|
|
|
|
analyzer.load(mainPath);
|
|
|
|
analyzer.finishLoading();
|
|
|
|
// coalesce the results
|
|
var dependencies = analyzer.mapGlobalsToFiles();
|
|
|
|
// log identifer => files defining
|
|
stream.print("Global defines:");
|
|
Object.keys(dependencies).sort().forEach(function(identifier) {
|
|
stream.print("\0blue(" + identifier + "\0) => \0cyan(" + dependencies[identifier].map(rootPath.relative.bind(rootPath)) + "\0)");
|
|
});
|
|
|
|
// phase 2: walk the dependency tree (both imports and references) to determine exactly which files need to be included
|
|
stream.print("\0red(PHASE 2:\0) Traverse dependency graph...");
|
|
|
|
var requiredFiles = {};
|
|
requiredFiles[mainPath] = true;
|
|
|
|
var context = {
|
|
ignoreFrameworkImports : true, // ignores "XXX/XXX.j" imports
|
|
importCallback: function(importing, imported) { requiredFiles[imported] = true; },
|
|
referenceCallback: function(referencing, referenced) { requiredFiles[referenced] = true; },
|
|
progressCallback: function(relPath) { stream.print("Processing \0cyan("+relPath+"\0)"); },
|
|
ignoreFrameworkImportsCallback : function(relPath) { stream.print("\0yellow(Ignoring imports in "+relPath+"\0)"); }
|
|
}
|
|
|
|
mainExecutable = analyzer.executableForImport(mainPath);
|
|
|
|
// check the code
|
|
analyzer.traverseDependencies(mainExecutable, context);
|
|
|
|
// check the cibs
|
|
var globalsToFiles = analyzer.mapGlobalsToFiles();
|
|
cibs.forEach(function(cibPath) {
|
|
var cibClasses = findCibClassDependencies(cibPath);
|
|
stream.print("Cib: \0green("+rootPath.relative(cibPath)+"\0) => \0cyan("+cibClasses+"\0)");
|
|
|
|
var referencedFiles = {};
|
|
markFilesReferencedByTokens(cibClasses, globalsToFiles, referencedFiles);
|
|
analyzer.checkReferenced(context, null, referencedFiles);
|
|
});
|
|
|
|
var included = 0, total = 0;
|
|
var includedBytes = 0, totalBytes = 0, dataBytes = 0, mhtmlBytes = 0;
|
|
|
|
_OBJJ.FileExecutable.allFileExecutables().forEach(function(aFileExecutable) {
|
|
var path = aFileExecutable.path();
|
|
|
|
// mark all ".keytheme"s as required
|
|
if (/\.keyedtheme$/.test(path))
|
|
requiredFiles[path] = true;
|
|
|
|
if (requiredFiles[path]) {
|
|
stream.print("Included: \0green(" + rootPath.relative(path) + "\0)");
|
|
included++;
|
|
includedBytes += aFileExecutable.code().length
|
|
}
|
|
else {
|
|
stream.print("Excluded: \0red(" + rootPath.relative(path) + "\0)");
|
|
}
|
|
total++;
|
|
totalBytes += aFileExecutable.code().length
|
|
}, this);
|
|
stream.print(_OBJJ.sprintf(
|
|
"Saved \0green(%f%%\0) (\0blue(%s\0)); Total required files: \0magenta(%d\0) (\0blue(%s\0)) of \0magenta(%d\0) (\0blue(%s\0));",
|
|
Math.round(((includedBytes - totalBytes) / totalBytes) * -100),
|
|
bytesToString(totalBytes - includedBytes),
|
|
included, bytesToString(includedBytes),
|
|
total, bytesToString(totalBytes)
|
|
));
|
|
|
|
// phase 3b: rebuild .sj files with correct imports, copy .j files
|
|
stream.print("\0red(PHASE 3b:\0) Rebuild .sj files");
|
|
|
|
var processedBundles = {};
|
|
|
|
for (var path in requiredFiles)
|
|
{
|
|
var executable = analyzer.executableForImport(path),
|
|
bundle = analyzer.context.global.CFBundle.bundleContainingURL(executable.path()),
|
|
relativePath = FILE.relative(FILE.join(bundle.path(), ""), executable.path());
|
|
|
|
if (executable.path() !== path)
|
|
CPLog.warn("Sanity check failed (file path): " + executable.path() + " vs. " + path);
|
|
|
|
if (bundle && bundle.infoDictionary())
|
|
{
|
|
var executablePath = bundle.executablePath();
|
|
if (executablePath)
|
|
{
|
|
if (context.ignoredImports[path])
|
|
{
|
|
stream.print("Stripping extra imports from \0blue(" + path + "\0)");
|
|
|
|
var code = executable.code();
|
|
var dependencies = executable.fileDependencies(),
|
|
fileExecutableSearcher = executable.fileExecutableSearcher();
|
|
for (var i = 0; i < dependencies.length; i++) {
|
|
var dependency = dependencies[i];
|
|
var dependencyExecutable = nil;
|
|
|
|
fileExecutableSearcher(dependency.URL(), dependency.isLocal(), function(aFileExecutable)
|
|
{
|
|
dependencyExecutable = aFileExecutable;
|
|
});
|
|
|
|
var dependencyPath = dependencyExecutable.path();
|
|
if (!requiredFiles[dependencyPath]) {
|
|
stream.print(" -> \0red(" + dependencyPath + "\0)");
|
|
// build up a regex to match objj_executeFile with optional whitespace
|
|
var regex = new RegExp([
|
|
RegExp.escape("objj_executeFile"),
|
|
RegExp.escape("("),
|
|
"[\"']"+RegExp.escape(dependency.path())+"[\"']",
|
|
RegExp.escape(","),
|
|
dependency.isLocal() ? "(true|YES)" : "(false|NO)",
|
|
RegExp.escape(")")
|
|
].join("\\s*"), "g");
|
|
// replace instances of "objj_executeFile()" with "(undefined)"
|
|
newCode = code.replace(regex, "/* $& */ (undefined)");
|
|
if (newCode === code)
|
|
stream.print("\0yellow(Warning:\0) Unable to strip import for \0cyan(" + dependency.path() + "\0) (" + dependency.isLocal() + ")");
|
|
code = newCode;
|
|
// remove from dependencies list (decrement i since we're removing an item)
|
|
dependencies.splice(i--, 1);
|
|
}
|
|
}
|
|
if (code !== executable.code())
|
|
executable.setCode(code);
|
|
}
|
|
|
|
if (!outputFiles[executablePath])
|
|
{
|
|
outputFiles[executablePath] = [];
|
|
outputFiles[executablePath].push("@STATIC;1.0;");
|
|
}
|
|
|
|
var fileContents = executable.toMarkedString();
|
|
|
|
outputFiles[executablePath].push("p;" + relativePath.length + ";" + relativePath);
|
|
outputFiles[executablePath].push("t;" + fileContents.length + ";" + fileContents);
|
|
|
|
stream.print("Adding \0green(" + rootPath.relative(path) + "\0) to \0cyan(" + rootPath.relative(executablePath) + "\0)");
|
|
}
|
|
else
|
|
{
|
|
stream.print("Passing .j through: \0green(" + rootPath.relative(path) + "\0)");
|
|
}
|
|
|
|
if (!processedBundles[bundle.path()] && bundle.hasSpritedImages())
|
|
{
|
|
processedBundles[bundle.path()] = bundle;
|
|
stream.print("Sizing bundle's sprited resources \0green(" + bundle.path() + "\0)");
|
|
|
|
var bundlePath = FILE.join(bundle.path(), environment+".environment"),
|
|
bundleDataPath = FILE.join(bundlePath, "dataURLs.txt"),
|
|
bundleMHTMLPath = FILE.join(bundlePath, "MHTMLPaths.txt");
|
|
|
|
dataBytes += FILE.read(bundleDataPath, {charset : "UTF-8"}).length;
|
|
mhtmlBytes += FILE.read(bundleMHTMLPath, {charset : "UTF-8"}).length;
|
|
}
|
|
}
|
|
else
|
|
CPLog.warn("No bundle (or info dictionary for) " + rootPath.relative(path));
|
|
}
|
|
|
|
return {executable:includedBytes, data:dataBytes, mhtml:mhtmlBytes};
|
|
}
|
|
|
|
function pngcrushDirectory(directory)
|
|
{
|
|
var directoryPath = FILE.path(directory),
|
|
pngs = directoryPath.glob("**/*.png");
|
|
|
|
system.stderr.print("Running pngcrush on " + pngs.length + " pngs:");
|
|
pngs.forEach(function(dstPath)
|
|
{
|
|
var tmpPath = FILE.path(dstPath+".tmp"),
|
|
p = OS.popen(["pngcrush", "-rem", "alla", "-reduce", /*"-brute",*/ dstPath, tmpPath]);
|
|
|
|
if (p.wait())
|
|
{
|
|
CPLog.warn("pngcrush failed. Ensure it's installed and on your PATH.");
|
|
}
|
|
else
|
|
{
|
|
FILE.move(tmpPath, dstPath);
|
|
system.stderr.write(".").flush();
|
|
}
|
|
|
|
p.stdin.close();
|
|
p.stdout.close();
|
|
p.stderr.close();
|
|
|
|
});
|
|
system.stderr.print("");
|
|
}
|
|
|
|
function bytesToString(bytes) {
|
|
var n = 0;
|
|
while (bytes > 1024) {
|
|
bytes /= 1024;
|
|
n++;
|
|
}
|
|
return Math.round(bytes*100)/100 + " " + ["", "K", "M"][n] + "B";
|
|
}
|