TeaSpeak-Client/installer/build.ts
2019-06-26 22:09:01 +02:00

344 lines
11 KiB
TypeScript

import {Options} from "electron-packager";
import * as packager from "electron-packager"
const pkg = require('../package.json');
const dev_dependencies = Object.keys(pkg.devDependencies);
import * as fs from "fs-extra";
import * as path_helper from "path";
import {parse_version} from "../modules/shared/version";
import * as util from "util";
import * as child_process from "child_process";
import * as os from "os";
import * as asar from "asar";
import * as querystring from "querystring";
import request = require("request");
let options: Options = {} as any;
let version = parse_version(pkg.version);
version.timestamp = Date.now();
options.dir = '.';
options.name = "TeaClient";
options.executableName = "TeaClient";
options.appVersion = pkg.version;
options.appCopyright = "© 2018-2019 Markus Hadenfeldt All Rights Reserved";
options.out = "build/";
if(!pkg.dependencies['electron']) {
console.error("Missing electron version");
process.exit(1);
}
options.electronVersion = pkg.dependencies['electron'];
options.protocols = [{name: "TeaSpeak - Connect", schemes: ["teaserver"]}];
options.overwrite = true;
options.derefSymlinks = true;
options.buildVersion = version.toString(true);
options.asar = {
unpackDir: "teaclient-unpacked"
};
interface ProjectEntry {
type: ProjectEntryType;
}
interface ProjectFile extends ProjectEntry {
path: string;
name: string | RegExp;
//target_name?: string | ((file: string) => string);
}
interface ProjectDirectory extends ProjectEntry {
path: string | RegExp;
children?: boolean;
files?: RegExp;
}
enum ProjectEntryType {
FILE,
DIRECTORY
}
const project_files: ProjectEntry[] = [];
{ /* general required files*/
project_files.push({
type: ProjectEntryType.FILE,
path: "/",
name: "package.json"
} as ProjectFile);
project_files.push({
type: ProjectEntryType.FILE,
path: "/",
name: "main.js"
} as ProjectFile);
project_files.push({
type: ProjectEntryType.DIRECTORY,
path: "/node_modules",
children: true,
files: /\.(js|css|html|node|json)$/
} as ProjectDirectory);
project_files.push({
type: ProjectEntryType.DIRECTORY,
path: "/node_modules/electron",
children: true,
files: /.*/
} as ProjectDirectory);
}
/* TeaClient modules */
project_files.push({
type: ProjectEntryType.DIRECTORY,
path: "/modules",
children: true,
files: /.*\.(js|css|html|png|svg)$/
} as ProjectDirectory);
/* resource files */
project_files.push({
type: ProjectEntryType.DIRECTORY,
path: "/resources"
} as ProjectDirectory);
if (process.argv[2] == "linux") {
options.arch = "x64";
options.platform = "linux";
options.icon = "resources/logo.svg";
} else if (process.argv[2] == "win32") {
options.arch = "x64";
options.platform = "win32";
options.icon = "resources/logo.ico";
} else {
console.error("Invalid system");
process.exit(1);
}
const path_validator = (path: string) => {
path = path.replace(/\\/g,"/");
const IGNORE = true;
const APPEND = false;
const ppath = path_helper.parse(path);
const is_directory = ppath.ext == '' && fs.statSync(path_helper.join('.', ppath.dir, ppath.name)).isDirectory();
const directory = (is_directory ? path_helper.join(ppath.dir, ppath.name) : ppath.dir).replace(/\\/g,"/");
//console.log("Is directory %o => %s", is_directory, directory);
//console.dir(ppath);
for(const entry of project_files) {
if(entry.type == ProjectEntryType.DIRECTORY) {
const dir_entry = <ProjectDirectory>entry;
if(typeof(dir_entry.path) === 'string') {
//ppath.dir == dir_entry.path
//console.log("'" + dir_entry.path + "' | '" + directory + "'");
if(dir_entry.path.startsWith(directory)) {
if(is_directory)
return APPEND;
}
if(directory == dir_entry.path) {
//console.log("Math: " + ppath.base + " to " + dir_entry.files);
if(dir_entry.files)
return ppath.base.match(dir_entry.files) ? APPEND : IGNORE;
return APPEND;
}
if(directory.startsWith(dir_entry.path)) {
if(dir_entry.children) {
if(is_directory)
return APPEND;
const sub_path = directory.substr(dir_entry.path.length);
//console.log("Sub path: " + sub_path + ". Test: " + (path_helper.join(sub_path, ppath.base) + " against " + dir_entry.files);
if(dir_entry.files)
return path_helper.join(sub_path, ppath.base).match(dir_entry.files) ? APPEND : IGNORE;
return APPEND;
}
//TODO test for regex
}
} else {
//TODO
}
} else if(entry.type == ProjectEntryType.FILE) {
const file_entry = <ProjectFile>entry;
if(file_entry.path.startsWith(directory) && is_directory)
return APPEND;
if(directory == file_entry.path) {
if(typeof(file_entry.name) === 'string' && ppath.base == file_entry.name)
return APPEND;
else if (ppath.base.match(file_entry.name))
return APPEND;
}
}
}
return true;
};
options.ignore = path => {
if(path.length == 0) return false; //Dont ignore root paths
const ignore_path = path_validator(path);
if(!ignore_path) {
console.log(" + " + path);
}
return ignore_path;
};
async function copy_striped(source: string, target: string, symbol_directory: string) {
const copy_file = util.promisify(fs.copyFile);
const exec = util.promisify(child_process.exec);
if(process.argv[2] == "win32") {
await copy_file(source, target);
return;
}
if(process.argv[2] != "linux") throw "invalid target type";
await copy_file(source, target);
{
const symbols_command = await exec("dump_syms " + target, {
maxBuffer: 1024 * 1024 * 512
});
if(symbols_command.stderr.length != 0) {
console.error("Failed to create sys dump: %o", symbols_command.stderr.toString());
throw "symbol create failed";
}
const symbols = symbols_command.stdout.toString();
const header = symbols.substr(0, symbols.indexOf('\n'));
const binary_name = path_helper.basename(target);
const dump_id = header.split(" ")[3];
if(binary_name != header.split(" ")[4])
throw "binary name missmatch";
const dump_dir = path_helper.join(symbol_directory, binary_name, dump_id);
const dump_file = path_helper.join(dump_dir, binary_name + ".sym");
if(!(await fs.pathExists(dump_dir)))
await fs.mkdirp(dump_dir);
await fs.ensureDir(dump_dir);
console.log("Writing file to %s", dump_file);
await fs.writeFile(dump_file, symbols);
console.log("Created dump file for binary %s (%s).", binary_name, dump_id);
}
{
console.log("Striping file");
//TODO: Keep node module names!
const strip_command = await exec("strip -s " + target, {
maxBuffer: 1024 * 1024 * 512
});
if(strip_command.stderr.length != 0) {
console.error("Failed to strip binary: %o", strip_command.stderr.toString());
throw "strip failed";
}
const nm_command = await exec("nm " + target, {
maxBuffer: 1024 * 1024 * 512
});
console.log("File stripped. Symbols left: \n%s", nm_command.stderr.toString().trim() || nm_command.stdout.toString().trim());
}
}
async function create_native_addons(target_directory: string, symbol_directory: string) {
let node_source = "native/build/" + os.platform() + "_" + os.arch() + "/";
console.log("Native addon path: %s", node_source);
await fs.ensureDir(target_directory);
for(const file of await fs.readdir(node_source)) {
if(!file.endsWith(".node")) {
console.warn("Discovered non node file within node file out dir");
continue;
}
await copy_striped(path_helper.join(node_source, file), path_helper.join(target_directory, file), symbol_directory);
}
}
interface UIVersion {
channel: string;
version: string;
git_hash: string;
timestamp: number;
required_client?: string;
filename?: string;
}
async function create_default_ui_pack(target_directory: string) {
const remote_url = "https://clientapi.teaspeak.de/";
const channel = "release";
const file = path_helper.join(target_directory, "default_ui.tar.gz");
console.log("Creating default UI pack. Downloading from %s (channel: %s)", remote_url, channel);
await fs.ensureDir(target_directory);
let ui_info: UIVersion;
await new Promise((resolve, reject) => {
request.get(remote_url + "api.php?" + querystring.stringify({
type: "ui-download",
channel: channel,
version: "latest"
}), {
timeout: 5000
}).on('response', function(response) {
if(response.statusCode != 200)
reject("Failed to download UI files (Status code " + response.statusCode + ")");
ui_info = {
channel: channel,
version: response.headers["x-ui-version"] as string,
git_hash: response.headers["x-ui-git-ref"] as string,
required_client: response.headers["x-ui-required_client"] as string,
timestamp: parseInt(response.headers["x-ui-timestamp"] as string),
filename: path_helper.basename(file)
}
}).on('error', error => {
reject("Failed to download UI files: " + error);
}).pipe(fs.createWriteStream(file)).on('finish', resolve);
});
if(!ui_info)
throw "failed to generate ui info!";
await fs.writeJson(path_helper.join(target_directory, "default_ui_info.json"), ui_info);
console.log("UI-Pack downloaded!");
}
let path;
packager(options).then(async app_paths => {
console.log("Copying changelog file!");
await util.promisify(fs.copyFile)(path_helper.join(options.dir, "github", "ChangeLog.txt"), path_helper.join(app_paths[0], "ChangeLog.txt"));
return app_paths;
}).then(async app_paths => {
await create_native_addons(path_helper.join(app_paths[0], "resources", "natives"), "build/symbols");
return app_paths;
}).then(async app_paths => {
await create_default_ui_pack(path_helper.join(app_paths[0], "resources", "ui"));
return app_paths;
}).then(async appPaths => {
///native/build/linux_amd64
path = appPaths[0];
if(process.argv[2] == "linux") {
await copy_striped(options.dir + "/native/build/exe/update-installer", path + "/update-installer", "build/symbols");
} else if (process.argv[2] == "win32") {
await copy_striped(options.dir + "/native/build/exe/update-installer.exe", path + "/update-installer.exe", "build/symbols");
}
return fs.writeJson(path + "/app_version.json", {
version: version.toString(true),
timestamp: version.timestamp
});
}).then(() => {
console.log("Package created");
});
export {}