diff --git a/src/cmds.ts b/src/cmds.ts new file mode 100644 index 0000000..6809a31 --- /dev/null +++ b/src/cmds.ts @@ -0,0 +1,161 @@ +import { + bootstrap, + build, + discard, + download, + downloadArtifacts, + execute, + exportFile, + exportPatches, + fixLineEndings, + importPatches, + init, + licenseCheck, + melonPackage, + reset, + run, + setBranch, + status, + test +} from "./commands"; +import { Cmd } from "./types"; + +export const commands: Cmd[] = [ + { + cmd: "bootstrap", + description: "Bootstrap Dot Browser.", + controller: bootstrap + }, + { + cmd: "build [os]", + aliases: ["b"], + description: + "Build Dot Browser. Specify the OS param for cross-platform builds.", + options: [ + { + arg: "--a, --arch ", + description: + "Specify architecture for build" + } + ], + controller: build + }, + { + cmd: "discard ", + description: "Discard a files changes.", + options: [ + { + arg: "--keep, --keep-patch", + description: + "Keep the patch file instead of removing it" + } + ], + controller: discard + }, + { + cmd: "download [ffVersion]", + description: "Download Firefox.", + controller: download + }, + { + cmd: "download-artifacts", + description: + "Download Windows artifacts from GitHub.", + flags: { + platforms: ["win32"] + }, + controller: downloadArtifacts + }, + { + cmd: "execute", + description: + "Execute a command inside the engine directory.", + controller: execute + }, + { + cmd: "export-file ", + description: "Export a changed file as a patch.", + controller: exportFile + }, + { + cmd: "export", + aliases: ["export-patches"], + description: + "Export the changed files as patches.", + controller: exportPatches + }, + { + cmd: "lfify", + aliases: ["fix-le"], + description: + "Convert CRLF line endings to Unix LF line endings.", + controller: fixLineEndings + }, + { + cmd: "import [type]", + aliases: ["import-patches", "i"], + description: "Import patches into the browser.", + options: [ + { + arg: "-m, --minimal", + description: + "Import patches in minimal mode" + }, + { + arg: "--noignore", + description: + "Bypass .gitignore. You shouldn't really use this." + } + ], + controller: importPatches + }, + { + cmd: "init ", + aliases: ["initialise", "initialize"], + description: "Initialise the Firefox directory.", + controller: init + }, + { + cmd: "license-check", + aliases: ["lc"], + description: + "Check the src directory for the absence of MPL-2.0 header.", + controller: licenseCheck + }, + { + cmd: "package", + aliases: ["pack"], + description: + "Package the browser for distribution.", + controller: melonPackage + }, + { + cmd: "reset", + description: + "Reset the source directory to stock Firefox.", + controller: reset + }, + { + cmd: "run [chrome]", + aliases: ["r", "open"], + description: "Run the browser.", + controller: run + }, + { + cmd: "set-branch ", + description: "Change the default branch.", + controller: setBranch + }, + { + cmd: "status", + description: + "Status and files changed for src directory.", + controller: status + }, + { + cmd: "test", + description: + "Run the test suite for Dot Browser.", + controller: test + } +]; diff --git a/src/commands/bootstrap.ts b/src/commands/bootstrap.ts new file mode 100644 index 0000000..ef946c7 --- /dev/null +++ b/src/commands/bootstrap.ts @@ -0,0 +1,89 @@ +/// + +import distro from "linus"; +import { bin_name } from ".."; +import { log } from "../"; +import { ENGINE_DIR } from "../constants"; +import { dispatch } from "../utils"; +import { pacmanInstall } from "./bootstrap/arch"; + +export const bootstrap = async () => { + if (process.platform == "win32") + log.error( + `You do not need to bootstrap on Windows. As long as you ran |${bin_name} download-artifacts| everything should work fine.` + ); + + log.info(`Bootstrapping Dot Browser for Desktop...`); + + const args = ["--application-choice", "browser"]; + + if (process.platform === "linux") { + linuxBootstrap(); + } else { + console.info( + `Custom bootstrapping doesn't work on ${process.platform}. Consider contributing to improve support` + ); + + console.info( + `Passing through to |mach bootstrap|` + ); + + await dispatch( + `./mach`, + ["bootstrap", ...args], + ENGINE_DIR + ); + } +}; + +function getDistro(): Promise { + return new Promise((resolve, reject) => { + distro.name((err: Error, name: string) => { + if (name) resolve(name); + else { + reject( + err || "Failed to get linux distro" + ); + } + }); + }); +} + +async function linuxBootstrap() { + const distro = await getDistro(); + + switch (distro) { + // Both arch and manjaro use the same package repo and the same package manager + case "ManjaroLinux": + case "ArchLinux": + console.log( + await pacmanInstall( + // Shared packages + "base-devel", + "nodejs", + "unzip", + "zip", + + // Needed for desktop apps + "alsa-lib", + "dbus-glib", + "gtk3", + "libevent", + "libvpx", + "libxt", + "mime-types", + "nasm", + "startup-notification", + "gst-plugins-base-libs", + "libpulse", + "xorg-server-xvfb", + "gst-libav", + "gst-plugins-good" + ) + ); + break; + + default: + log.error(`Unimplemented distro '${distro}'`); + } +} diff --git a/src/commands/bootstrap/arch.ts b/src/commands/bootstrap/arch.ts new file mode 100644 index 0000000..ee991dc --- /dev/null +++ b/src/commands/bootstrap/arch.ts @@ -0,0 +1,14 @@ +import execa from "execa"; + +export async function pacmanInstall( + ...packages: string[] +): Promise { + return ( + await execa("sudo", [ + "pacman", + "--noconfirm", + "-S", + ...packages + ]) + ).stdout; +} diff --git a/src/commands/build.ts b/src/commands/build.ts new file mode 100644 index 0000000..3fdf0dd --- /dev/null +++ b/src/commands/build.ts @@ -0,0 +1,187 @@ +import execa from "execa"; +import { + existsSync, + readFileSync, + writeFileSync +} from "fs"; +import { join, resolve } from "path"; +import { bin_name, log } from ".."; +import { + ARCHITECTURE, + BUILD_TARGETS, + CONFIGS_DIR, + ENGINE_DIR +} from "../constants"; +import { dispatch } from "../utils"; + +const platform: any = { + win32: "windows", + darwin: "macos", + linux: "linux" +}; + +const applyConfig = async (os: string, arch: string) => { + log.info("Applying mozconfig..."); + + let commonConfig = readFileSync( + resolve(CONFIGS_DIR, "common", "mozconfig"), + "utf-8" + ); + + const changesetPrefix = commonConfig + .split("\n") + .find((ln) => + ln.startsWith("export MOZ_SOURCE_CHANGESET=") + ); + + const changeset = changesetPrefix?.replace( + /export MOZ_SOURCE_CHANGESET=/, + "" + ); + + const { stdout: gitSha } = await execa("git", [ + "rev-parse", + "HEAD" + ]); + + console.log(changeset, gitSha); + + if (changeset) + commonConfig = commonConfig.replace( + changeset, + gitSha + ); + + writeFileSync( + resolve(CONFIGS_DIR, "common", "mozconfig"), + commonConfig + ); + + const osConfig = readFileSync( + resolve( + CONFIGS_DIR, + os, + arch === "i686" + ? "mozconfig-i686" + : "mozconfig" + ), + "utf-8" + ); + + // Allow a custom config to be placed in /mozconfig. This will not be committed + // to origin + const customConfig = existsSync( + join(process.cwd(), "mozconfig") + ) + ? readFileSync( + join(process.cwd(), "mozconfig") + ).toString() + : ""; + + const mergedConfig = `# This file is automatically generated. You should only modify this if you know what you are doing!\n\n${commonConfig}\n\n${osConfig}\n\n${customConfig}`; + + writeFileSync( + resolve(ENGINE_DIR, "mozconfig"), + mergedConfig + ); + + log.info(`Config for this \`${os}\` build:`); + + mergedConfig.split("\n").map((ln) => { + if ( + ln.startsWith("mk") || + ln.startsWith("ac") || + ln.startsWith("export") + ) + log.info( + `\t${ln + .replace(/mk_add_options /, "") + .replace(/ac_add_options /, "") + .replace(/export /, "")}` + ); + }); +}; + +const genericBuild = async (os: string, tier: string) => { + log.info(`Building for "${os}"...`); + + log.warning( + `If you get any dependency errors, try running |${bin_name} bootstrap|.` + ); + + await dispatch( + `./mach`, + ["build"].concat(tier ? [tier] : []), + ENGINE_DIR + ); +}; + +const parseDate = (d: number) => { + d = d / 1000; + var h = Math.floor(d / 3600); + var m = Math.floor((d % 3600) / 60); + var s = Math.floor((d % 3600) % 60); + + var hDisplay = + h > 0 + ? h + (h == 1 ? " hour, " : " hours, ") + : ""; + var mDisplay = + m > 0 + ? m + (m == 1 ? " minute, " : " minutes, ") + : ""; + var sDisplay = + s > 0 + ? s + (s == 1 ? " second" : " seconds") + : ""; + return hDisplay + mDisplay + sDisplay; +}; + +const success = (date: number) => { + // mach handles the success messages + console.log(); + log.info( + `Total build time: ${parseDate( + Date.now() - date + )}.` + ); +}; + +interface Options { + arch: string; +} + +export const build = async ( + tier: string, + options: Options +) => { + let d = Date.now(); + + // Host build + + const prettyHost = platform[process.platform as any]; + + if (BUILD_TARGETS.includes(prettyHost)) { + let arch = "64bit"; + + if (options.arch) { + if (!ARCHITECTURE.includes(options.arch)) + return log.error( + `We do not support "${ + options.arch + }" build right now.\nWe only currently support ${JSON.stringify( + ARCHITECTURE + )}.` + ); + else arch = options.arch; + } + + applyConfig(prettyHost, options.arch); + + setTimeout(async () => { + await genericBuild(prettyHost, tier).then( + (_) => success(d) + ); + }, 2500); + } +}; diff --git a/src/commands/discard.ts b/src/commands/discard.ts new file mode 100644 index 0000000..6471668 --- /dev/null +++ b/src/commands/discard.ts @@ -0,0 +1,71 @@ +import execa from "execa"; +import { existsSync, statSync } from "fs"; +import { resolve } from "path"; +import rimraf from "rimraf"; +import { log } from ".."; +import { ENGINE_DIR, PATCHES_DIR } from "../constants"; + +interface Options { + keep?: boolean; + fromRemote?: string; +} + +const remotes = { + ff: (file: string, version: string) => + `https://hg.mozilla.org/experimental/firefox-unified-stage/raw-file/FIREFOX_${version + .replace(" ", "_") + .replace(".", "_")}_RELEASE/${file}`, + dot: (file: string, ref: string) => + `https://raw.githubusercontent.com/dothq/browser-desktop/${ref}/${file}` +}; + +export const discard = async ( + file: string, + options: Options +) => { + log.info(`Discarding ${file}...`); + + if (!statSync(file).isFile()) + throw new Error("Target must be a file."); + + // @todo add remote discard + if (options.fromRemote) { + if ( + options.fromRemote == "ff" || + options.fromRemote == "firefox" + ) { + } else if (options.fromRemote == "dot") { + } else { + throw new Error( + "Unrecognised remote type. Expected `ff` or `dot`." + ); + } + } else { + if (!existsSync(resolve(ENGINE_DIR, file))) + throw new Error( + `File ${file} could not be found in src directory. Check the path for any mistakes and try again.` + ); + + const patchFile = resolve( + PATCHES_DIR, + file.replace(/\//g, "-").replace(/\./g, "-") + + ".patch" + ); + + if (!existsSync(patchFile)) + throw new Error( + `File ${file} does have an associated patch in the patches directory.` + ); + + const { stdout, exitCode } = await execa( + "git", + ["apply", "-R", "-p", "1", patchFile], + { cwd: ENGINE_DIR } + ); + + if (exitCode == 0) { + log.success(`Discarded changes to ${file}.`); + if (!options.keep) rimraf.sync(patchFile); + } else throw new Error(stdout); + } +}; diff --git a/src/commands/download-artifacts.ts b/src/commands/download-artifacts.ts new file mode 100644 index 0000000..66dea58 --- /dev/null +++ b/src/commands/download-artifacts.ts @@ -0,0 +1,81 @@ +import axios from "axios"; +import execa from "execa"; +import fs from "fs"; +import { homedir } from "os"; +import { posix, resolve, sep } from "path"; +import { log } from ".."; + +export const downloadArtifacts = async () => { + if (process.platform !== "win32") + return log.error( + "This is not a Windows machine, will not download artifacts." + ); + if (process.env.MOZILLABUILD) + return log.error( + "Run this command in Git Bash, it does not work in Mozilla Build." + ); + + const filename = "mozbuild.tar.bz2"; + const url = `https://github.com/dothq/windows-artifacts/releases/latest/download/mozbuild.tar.bz2`; + let home = homedir().split(sep).join(posix.sep); + + if (process.platform == "win32") { + home = + "/" + + home + .replace(/\:/, "") + .replace(/\\/g, "/") + .toLowerCase(); + } + + log.info(`Downloading Windows artifacts...`); + + const { data, headers } = await axios.get(url, { + responseType: "stream" + }); + + const length = headers["content-length"]; + + const writer = fs.createWriteStream( + resolve(process.cwd(), filename) + ); + + let receivedBytes = 0; + + data.on("data", (chunk: any) => { + receivedBytes += chunk.length; + + let rand = Math.floor(Math.random() * 1000 + 1); + + if (rand > 999.5) { + let percentCompleted = parseInt( + Math.round( + (receivedBytes * 100) / length + ).toFixed(0) + ); + if ( + percentCompleted % 2 == 0 || + percentCompleted >= 100 + ) + return; + log.info( + `\t${filename}\t${percentCompleted}%...` + ); + } + }); + + data.pipe(writer); + + data.on("end", async () => { + log.info("Unpacking mozbuild..."); + + await execa("tar", [ + "-xvf", + filename, + "-C", + home + ]); + + log.info("Done extracting mozbuild artifacts."); + }); +}; diff --git a/src/commands/download.ts b/src/commands/download.ts new file mode 100644 index 0000000..3f8a5ff --- /dev/null +++ b/src/commands/download.ts @@ -0,0 +1,269 @@ +import axios from "axios"; +import chalk from "chalk"; +import execa from "execa"; +import fs, { + existsSync, + rmdirSync, + writeFileSync +} from "fs"; +import { ensureDirSync, removeSync } from "fs-extra"; +import ora from "ora"; +import { homedir } from "os"; +import { posix, resolve, sep } from "path"; +import { bin_name, log } from ".."; +import { ENGINE_DIR } from "../constants"; +import { getLatestFF, writeMetadata } from "../utils"; +import { downloadArtifacts } from "./download-artifacts"; + +const pjson = require("../../package.json"); + +let initProgressText = "Initialising..."; +let initProgress: any = ora({ + text: `Initialising...`, + prefixText: chalk.blueBright.bold("00:00:00"), + spinner: { + frames: [""] + }, + indent: 0 +}); + +const onData = (data: any) => { + const d = data.toString(); + + d.split("\n").forEach((line: any) => { + if (line.trim().length !== 0) { + let t = line.split(" "); + t.shift(); + initProgressText = t.join(" "); + } + }); +}; + +const unpack = async (name: string, version: string) => { + let cwd = process.cwd().split(sep).join(posix.sep); + + if (process.platform == "win32") { + cwd = "./"; + } + + initProgress.start(); + + setInterval(() => { + if (initProgress) { + initProgress.text = initProgressText; + initProgress.prefixText = + chalk.blueBright.bold(log.getDiff()); + } + }, 100); + + initProgressText = `Unpacking Firefox...`; + + try { + rmdirSync(ENGINE_DIR); + } catch (e) {} + ensureDirSync(ENGINE_DIR); + + let tarProc = execa("tar", [ + "--transform", + "s,firefox-89.0,engine,", + `--show-transformed`, + "-xf", + resolve(cwd, ".dotbuild", "engines", name) + ]); + + (tarProc.stdout as any).on("data", onData); + (tarProc.stdout as any).on("error", onData); + + tarProc.on("exit", () => { + if (process.env.CI_SKIP_INIT) + return log.info("Skipping initialisation."); + + const initProc = execa(`./${bin_name}`, [ + "init", + "engine" + ]); + + (initProc.stdout as any).on("data", onData); + (initProc.stdout as any).on("error", onData); + + initProc.on("exit", async () => { + initProgressText = ""; + initProgress.stop(); + initProgress = null; + + await new Promise((resolve) => + setTimeout(resolve, 5000) + ); + + log.success( + `You should be ready to make changes to Dot Browser.\n\n\t You should import the patches next, run |${bin_name} import|.\n\t To begin building Dot, run |${bin_name} build|.` + ); + console.log(); + + pjson.versions["firefox-display"] = version; + pjson.versions["firefox"] = + version.split("b")[0]; + + writeFileSync( + resolve(process.cwd(), "package.json"), + JSON.stringify(pjson, null, 4) + ); + + await writeMetadata(); + + removeSync( + resolve(cwd, ".dotbuild", "engines", name) + ); + + process.exit(0); + }); + }); +}; + +export const download = async ( + firefoxVersion?: string +) => { + if (firefoxVersion) + log.warning( + `A custom Firefox version is being used. Some features of Dot may not work as expected.` + ); + + if (!firefoxVersion) { + firefoxVersion = + pjson.versions["firefox-display"]; + } + + let version = await getLatestFF(); + + if (firefoxVersion) { + version = firefoxVersion; + } + + const base = `https://archive.mozilla.org/pub/firefox/releases/${version}/source/`; + const filename = `firefox-${version}.source.tar.xz`; + + const url = `${base}${filename}`; + + log.info(`Locating Firefox release ${version}...`); + + ensureDirSync( + resolve(process.cwd(), `.dotbuild`, `engines`) + ); + + if ( + existsSync( + resolve( + process.cwd(), + `.dotbuild`, + `engines`, + `firefox-${version.split("b")[0]}` + ) + ) + ) { + log.error( + `Cannot download version ${ + version.split("b")[0] + } as it already exists at "${resolve( + process.cwd(), + `firefox-${version.split("b")[0]}` + )}"` + ); + } + + if (version == firefoxVersion) + log.info( + `Version is frozen at ${firefoxVersion}!` + ); + if (version.includes("b")) + log.warning( + "Version includes non-numeric characters. This is probably a beta." + ); + + if ( + fs.existsSync( + resolve( + process.cwd(), + `.dotbuild`, + `engines`, + "firefox", + version.split("b")[0] + ) + ) || + fs.existsSync( + resolve( + process.cwd(), + "firefox", + "firefox-" + version.split("b")[0] + ) + ) + ) + log.error( + `Workspace with version "${ + version.split("b")[0] + }" already exists.\nRemove that workspace and run |${bin_name} download ${version}| again.` + ); + + log.info(`Downloading Firefox release ${version}...`); + + const { data, headers } = await axios.get(url, { + responseType: "stream" + }); + + const length = headers["content-length"]; + + const writer = fs.createWriteStream( + resolve( + process.cwd(), + `.dotbuild`, + `engines`, + filename + ) + ); + + let receivedBytes = 0; + + data.on("data", (chunk: any) => { + receivedBytes += chunk.length; + + let rand = Math.floor(Math.random() * 1000 + 1); + + if (rand > 999.5) { + let percentCompleted = parseInt( + Math.round( + (receivedBytes * 100) / length + ).toFixed(0) + ); + if ( + percentCompleted % 2 == 0 || + percentCompleted >= 100 + ) + return; + log.info( + `\t${filename}\t${percentCompleted}%...` + ); + } + }); + + data.pipe(writer); + + data.on("end", async () => { + await unpack(filename, version); + + if (process.platform === "win32") { + if ( + existsSync( + resolve(homedir(), ".mozbuild") + ) + ) { + log.info( + "Mozbuild directory already exists, not redownloading" + ); + } else { + log.info( + "Mozbuild not found, downloading artifacts." + ); + await downloadArtifacts(); + } + } + }); +}; diff --git a/src/commands/execute.ts b/src/commands/execute.ts new file mode 100644 index 0000000..978ed1e --- /dev/null +++ b/src/commands/execute.ts @@ -0,0 +1,26 @@ +import { existsSync } from "fs"; +import { log } from ".."; +import { ENGINE_DIR } from "../constants"; +import { dispatch } from "../utils"; + +export const execute = async (_: any, cmd: any[]) => { + if (existsSync(ENGINE_DIR)) { + if (!cmd || cmd.length == 0) + log.error( + "You need to specify a command to run." + ); + + const bin = cmd[0]; + const args = cmd; + args.shift(); + + log.info( + `Executing \`${bin}${ + args.length !== 0 ? ` ` : `` + }${args.join(" ")}\` in \`src\`...` + ); + dispatch(bin, args, ENGINE_DIR, true); + } else { + log.error(`Unable to locate src directory.`); + } +}; diff --git a/src/commands/export-file.ts b/src/commands/export-file.ts new file mode 100644 index 0000000..2e285f5 --- /dev/null +++ b/src/commands/export-file.ts @@ -0,0 +1,64 @@ +import execa from "execa"; +import { existsSync, writeFileSync } from "fs"; +import { ensureDirSync } from "fs-extra"; +import { resolve } from "path"; +import { log } from ".."; +import { ENGINE_DIR, SRC_DIR } from "../constants"; +import { delay } from "../utils"; + +export const exportFile = async (file: string) => { + log.info(`Exporting ${file}...`); + + if (!existsSync(resolve(ENGINE_DIR, file))) + throw new Error( + `File ${file} could not be found in engine directory. Check the path for any mistakes and try again.` + ); + + const proc = await execa( + "git", + [ + "diff", + "--src-prefix=a/", + "--dst-prefix=b/", + "--full-index", + resolve(ENGINE_DIR, file) + ], + { + cwd: ENGINE_DIR, + stripFinalNewline: false + } + ); + const name = + file + .split("/") + [ + file.replace(/\./g, "-").split("/") + .length - 1 + ].replace(/\./g, "-") + ".patch"; + + const patchPath = file + .replace(/\./g, "-") + .split("/") + .slice(0, -1); + + ensureDirSync(resolve(SRC_DIR, ...patchPath)); + + if (proc.stdout.length >= 8000) { + log.warning(""); + log.warning( + `Exported patch is over 8000 characters. This patch may become hard to manage in the future.` + ); + log.warning( + `We recommend trying to decrease your patch size by making minimal edits to the source.` + ); + log.warning(""); + await delay(2000); + } + + writeFileSync( + resolve(SRC_DIR, ...patchPath, name), + proc.stdout + ); + log.info(`Wrote "${name}" to patches directory.`); + console.log(); +}; diff --git a/src/commands/export-patches.ts b/src/commands/export-patches.ts new file mode 100644 index 0000000..a3646e0 --- /dev/null +++ b/src/commands/export-patches.ts @@ -0,0 +1,218 @@ +import execa from "execa"; +import { + appendFileSync, + createWriteStream, + existsSync, + mkdirSync, + rmdirSync, + writeFileSync +} from "fs"; +import { copySync, ensureDirSync } from "fs-extra"; +import { resolve } from "path"; +import { log } from ".."; +import { + COMMON_DIR, + ENGINE_DIR, + PATCHES_DIR +} from "../constants"; +import manualPatches from "../manual-patches"; + +const flags: { + [key: string]: string; +} = { + D: "delete", + M: "modify", + A: "add" +}; + +const getFiles = async (flags: string, cwd: string) => { + let { stdout: ignored } = await execa( + "git", + [ + "ls-files", + `-${flags.toLowerCase()}`, + "-i", + "-o", + "--exclude-standard" + ], + { cwd } + ); + + let { stdout: fls } = await execa( + "git", + [ + "diff", + `--diff-filter=${flags}`, + "--name-only", + "--ignore-space-at-eol" + ], + { cwd } + ); + + const files = fls.split("\n").filter((i: any) => { + return !( + ignored.split("\n").includes(i) || + i == ".gitignore" + ); + }); // this filters out the manual patches + + log.info( + `Ignoring ${ignored.split("\n").length} files...` + ); + + const fileNames: any = files.map((f: any) => { + if (f.length !== 0) { + return ( + f + .replace(/\//g, "-") + .replace(/\./g, "-") + ".patch" + ); + } + }); + + return { files, fileNames }; +}; + +const exportModified = async ( + patchesDir: string, + cwd: string +) => { + const { files, fileNames } = await getFiles("M", cwd); + + var filesWritten = 0; + + await Promise.all( + files.map(async (file: any, i: any) => { + if (file) { + try { + const proc = execa( + "git", + [ + "diff", + "--src-prefix=a/", + "--dst-prefix=b/", + "--full-index", + file + ], + { + cwd, + stripFinalNewline: false + } + ); + const name = fileNames[i]; + + proc.stdout?.pipe( + createWriteStream( + resolve(patchesDir, name) + ) + ); + + appendFileSync( + resolve(PATCHES_DIR, ".index"), + `${name} - ${file}\n` + ); + + ++filesWritten; + } catch (e) { + log.error(e); + return; + } + } + }) + ); + + log.info( + `Wrote ${filesWritten} to patches directory.` + ); +}; + +const exportFlag = async ( + flag: string, + cwd: string, + actions: any[] +) => { + const { files } = await getFiles(flag, cwd); + + actions.push({ + action: flags[flag], + target: files + }); + + return actions; +}; + +const exportManual = async (cwd: string) => { + return new Promise(async (resol) => { + manualPatches.forEach((patch) => { + if (patch.action == "copy") { + if (typeof patch.src == "string") { + const inSrc = resolve(cwd, patch.src); + const outsideSrc = resolve( + COMMON_DIR, + patch.src + ); + + if (!existsSync(inSrc)) + return log.error( + `Cannot find "${patch.src}" from manual patches.` + ); + if (!existsSync(outsideSrc)) + ensureDirSync(outsideSrc); // make sure target dir exists before copying + + copySync(inSrc, outsideSrc); + } else if (Array.isArray(patch.src)) { + patch.src.forEach((p) => { + const inSrc = resolve(cwd, p); + const outsideSrc = resolve( + COMMON_DIR, + p + ); + + if (!existsSync(inSrc)) + return log.error( + `Cannot find "${p}" from manual patches.` + ); + if (!existsSync(outsideSrc)) + ensureDirSync(outsideSrc); // make sure target dir exists before copying + + copySync(inSrc, outsideSrc); + }); + } + } + }); + }); +}; + +export const exportPatches = async () => { + throw new Error( + "export-patches has been deprecated in favour of export-file. This change has been made to limit the amount of active patches we have in the tree." + ); + + let actions: any[] = []; + + log.info(`Wiping patches directory...`); + console.log(); + // TODO: Replace this with fs.rmSync(path, { recursive: true }) when node 12 is deprecated + // This function has been depriciated, however its replacement was only available + // from v14.14.0 onwards (https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fs_fs_rmsync_path_options) + rmdirSync(PATCHES_DIR, { recursive: true }); + mkdirSync(PATCHES_DIR); + writeFileSync(resolve(PATCHES_DIR, ".index"), ""); + + log.info("Exporting modified files..."); + await exportModified(PATCHES_DIR, ENGINE_DIR); + console.log(); + + log.info("Exporting deleted files..."); + await exportFlag("D", ENGINE_DIR, actions); + console.log(); + + log.info("Exporting manual patches..."); + await exportManual(ENGINE_DIR); + console.log(); + + copySync( + resolve(ENGINE_DIR, "dot"), + resolve(process.cwd(), "browser") + ); +}; diff --git a/src/commands/fix-le.ts b/src/commands/fix-le.ts new file mode 100644 index 0000000..a1c6187 --- /dev/null +++ b/src/commands/fix-le.ts @@ -0,0 +1,49 @@ +import { + existsSync, + readdirSync, + readFileSync +} from "fs-extra"; +import { resolve } from "path"; +import { log } from ".."; +import { ENGINE_DIR, PATCHES_DIR } from "../constants"; +import { dispatch } from "../utils"; + +export const fixLineEndings = async () => { + let patches = readdirSync(PATCHES_DIR); + + patches = patches.filter((p) => p !== ".index"); + + await Promise.all( + patches.map(async (patch) => { + const patchContents = readFileSync( + resolve(PATCHES_DIR, patch), + "utf-8" + ); + const originalPath = patchContents + .split("diff --git a/")[1] + .split(" b/")[0]; + + if ( + existsSync( + resolve(ENGINE_DIR, originalPath) + ) + ) { + dispatch( + "dos2unix", + [originalPath], + ENGINE_DIR + ).then(async (_) => { + await dispatch( + "dos2unix", + [patch], + PATCHES_DIR + ); + }); + } else { + log.warning( + `Skipping ${patch} as it no longer exists in tree...` + ); + } + }) + ); +}; diff --git a/src/commands/import-patches.ts b/src/commands/import-patches.ts new file mode 100644 index 0000000..0584cdb --- /dev/null +++ b/src/commands/import-patches.ts @@ -0,0 +1,141 @@ +import { sync } from "glob"; +import { bin_name, log } from ".."; +import { SRC_DIR } from "../constants"; +import Patch from "../controllers/patch"; +import manualPatches from "../manual-patches"; +import { delay, dispatch } from "../utils"; +const { + versions: { dot } +} = require("../../package.json"); + +const importManual = async ( + minimal?: boolean, + noIgnore?: boolean +) => { + log.info( + `Applying ${manualPatches.length} manual patches...` + ); + + if (!minimal) console.log(); + + await delay(500); + + return new Promise(async (res, rej) => { + var total = 0; + + var i = 0; + + for await (let { + name, + action, + src, + markers, + indent + } of manualPatches) { + ++i; + + const p = new Patch({ + name, + action, + src, + type: "manual", + status: [i, manualPatches.length], + markers, + indent, + options: { + minimal, + noIgnore + } + }); + + await delay(100); + + await p.apply(); + } + + log.success( + `Successfully imported ${manualPatches.length} manual patches!` + ); + console.log(); + + await delay(1000); + + res(total); + }); +}; + +const importPatchFiles = async ( + minimal?: boolean, + noIgnore?: boolean +) => { + let patches = sync("**/*.patch", { + nodir: true, + cwd: SRC_DIR + }); + + patches = patches + .filter((p) => p !== ".index") + .filter((p) => !p.includes("node_modules")); + + log.info(`Applying ${patches.length} patch files...`); + + if (!minimal) console.log(); + + await delay(500); + + var i = 0; + + for await (const patch of patches) { + ++i; + + const p = new Patch({ + name: patch, + type: "file", + status: [i, patches.length], + options: { + minimal, + noIgnore + } + }); + + await delay(100); + + await p.apply(); + } + + console.log(); + await dispatch( + `./${bin_name}`, + ["doctor", "patches"], + process.cwd(), + true, + true + ); + + log.success( + `Successfully imported ${patches.length} patch files!` + ); +}; + +interface Args { + minimal?: boolean; + noignore?: boolean; +} + +export const importPatches = async ( + type: string, + args: Args +) => { + if (type) { + if (type == "manual") + await importManual(args.minimal); + else if (type == "file") + await importPatchFiles(args.minimal); + } else { + await importManual(args.minimal, args.noignore); + await importPatchFiles( + args.minimal, + args.noignore + ); + } +}; diff --git a/src/commands/index.ts b/src/commands/index.ts new file mode 100644 index 0000000..5695b16 --- /dev/null +++ b/src/commands/index.ts @@ -0,0 +1,18 @@ +export * from "./bootstrap"; +export * from "./build"; +export * from "./discard"; +export * from "./download"; +export * from "./download-artifacts"; +export * from "./execute"; +export * from "./export-file"; +export * from "./export-patches"; +export * from "./fix-le"; +export * from "./import-patches"; +export * from "./init"; +export * from "./license-check"; +export * from "./package"; +export * from "./reset"; +export * from "./run"; +export * from "./set-branch"; +export * from "./status"; +export * from "./test"; diff --git a/src/commands/init.ts b/src/commands/init.ts new file mode 100644 index 0000000..98fa409 --- /dev/null +++ b/src/commands/init.ts @@ -0,0 +1,68 @@ +import { Command } from "commander"; +import { existsSync, readFileSync } from "fs"; +import { resolve } from "path"; +import { bin_name, log } from ".."; +import { dispatch } from "../utils"; + +export const init = async (directory: Command) => { + if (process.platform == "win32") { + // Because Windows cannot handle paths correctly, we're just calling a script as the workaround. + log.info( + "Successfully downloaded browser source. Please run |./windows-init.sh| to finish up." + ); + process.exit(0); + } + + const cwd = process.cwd(); + + const dir = resolve( + cwd as string, + directory.toString() + ); + + if (!existsSync(dir)) { + log.error( + `Directory "${directory}" not found.\nCheck the directory exists and run |${bin_name} init| again.` + ); + } + + let version = readFileSync( + resolve( + cwd, + directory.toString(), + "browser", + "config", + "version_display.txt" + ), + "utf-8" + ); + + if (!version) + log.error( + `Directory "${directory}" not found.\nCheck the directory exists and run |${bin_name} init| again.` + ); + + version = version.trim().replace(/\\n/g, ""); + + await dispatch("git", ["init"], dir as string); + await dispatch( + "git", + ["checkout", "--orphan", version], + dir as string + ); + await dispatch( + "git", + ["add", "-v", "-f", "."], + dir as string + ); + await dispatch( + "git", + ["commit", "-am", `"Firefox ${version}"`], + dir as string + ); + await dispatch( + "git", + ["checkout", "-b", "dot"], + dir as string + ); +}; diff --git a/src/commands/license-check.ts b/src/commands/license-check.ts new file mode 100644 index 0000000..0e64d08 --- /dev/null +++ b/src/commands/license-check.ts @@ -0,0 +1,92 @@ +import chalk from "chalk"; +import { readdirSync, readFileSync } from "fs-extra"; +import { resolve } from "path"; +import { log } from ".."; +import { ENGINE_DIR, PATCHES_DIR } from "../constants"; + +const ignoredExt = [".json", ".bundle.js"]; + +export const licenseCheck = async () => { + log.info("Checking project..."); + + let patches = readdirSync(PATCHES_DIR).map((p) => p); + + patches = patches.filter((p) => p !== ".index"); + + const originalPaths = patches.map((p) => { + const data = readFileSync( + resolve(PATCHES_DIR, p), + "utf-8" + ); + + return data + .split("diff --git a/")[1] + .split(" b/")[0]; + }); + + let passed: string[] = []; + let failed: string[] = []; + let ignored: string[] = []; + + originalPaths.forEach((p) => { + const data = readFileSync( + resolve(ENGINE_DIR, p), + "utf-8" + ); + const headerRegion = data + .split("\n") + .slice(0, 32) + .join(" "); + + const passes = + headerRegion.includes( + "http://mozilla.org/MPL/2.0" + ) && + headerRegion.includes( + "This Source Code Form" + ) && + headerRegion.includes("copy of the MPL"); + + const isIgnored = ignoredExt.find((i) => + p.endsWith(i) + ) + ? true + : false; + isIgnored && ignored.push(p); + + if (!isIgnored) { + if (passes) passed.push(p); + else if (!passes) failed.push(p); + } + }); + + let maxPassed = 5; + let i = 0; + + for (const p of passed) { + log.info( + `${p}... ${chalk.green("✔ Pass - MPL-2.0")}` + ); + + if (i >= maxPassed) { + log.info( + `${chalk.gray.italic( + `${ + passed.length - maxPassed + } other files...` + )} ${chalk.green("✔ Pass - MPL-2.0")}` + ); + break; + } + + ++i; + } + + failed.forEach((p, i) => { + log.info(`${p}... ${chalk.red("❗ Failed")}`); + }); + + ignored.forEach((p, i) => { + log.info(`${p}... ${chalk.gray("➖ Ignored")}`); + }); +}; diff --git a/src/commands/linus.d.ts b/src/commands/linus.d.ts new file mode 100644 index 0000000..eb507c9 --- /dev/null +++ b/src/commands/linus.d.ts @@ -0,0 +1,5 @@ +declare module "linus" { + export function name( + callback: (error: Error, name: string) => void + ): void; +} diff --git a/src/commands/package.ts b/src/commands/package.ts new file mode 100644 index 0000000..5136c38 --- /dev/null +++ b/src/commands/package.ts @@ -0,0 +1,35 @@ +import execa from "execa"; +import { existsSync } from "fs"; +import { resolve } from "path"; +import { bin_name, log } from ".."; +import { ENGINE_DIR } from "../constants"; + +export const melonPackage = async () => { + if (existsSync(ENGINE_DIR)) { + const artifactPath = resolve(ENGINE_DIR, "mach"); + + if (existsSync(artifactPath)) { + const args = ["package"]; + + log.info( + `Packaging \`dot\` with args ${JSON.stringify( + args.slice(1, 0) + )}...` + ); + + execa(artifactPath, args).stdout?.pipe( + process.stdout + ); + } else { + log.error( + `Cannot binary with name \`mach\` in ${resolve( + ENGINE_DIR + )}` + ); + } + } else { + log.error( + `Unable to locate any source directories.\nRun |${bin_name} download| to generate the source directory.` + ); + } +}; diff --git a/src/commands/reset.ts b/src/commands/reset.ts new file mode 100644 index 0000000..f727d43 --- /dev/null +++ b/src/commands/reset.ts @@ -0,0 +1,171 @@ +import execa from "execa"; +import { existsSync } from "fs-extra"; +import { resolve } from "path"; +import { confirm } from "promptly"; +import rimraf from "rimraf"; +import { bin_name, log } from ".."; +import { ENGINE_DIR } from "../constants"; +import { IPatch } from "../interfaces/patch"; +import manualPatches from "../manual-patches"; + +export const reset = async () => { + try { + log.warning( + "This will clear all your unexported changes in the `src` directory!" + ); + log.warning( + `You can export your changes by running |${bin_name} export|.` + ); + confirm(`Are you sure you want to continue?`, { + default: "false" + }) + .then(async (answer) => { + if (answer) { + await execa( + "git", + ["checkout", "."], + { cwd: ENGINE_DIR } + ); + + manualPatches.forEach( + async (patch: IPatch) => { + const { src, action } = patch; + + if (action == "copy") { + if ( + typeof src == "string" + ) { + const path = resolve( + ENGINE_DIR, + src + ); + + if ( + path !== + ENGINE_DIR + ) { + log.info( + `Deleting ${src}...` + ); + + if ( + existsSync( + path + ) + ) + rimraf.sync( + path + ); + } + } else if ( + Array.isArray(src) + ) { + src.forEach((i) => { + const path = + resolve( + ENGINE_DIR, + i + ); + + if ( + path !== + ENGINE_DIR + ) { + log.info( + `Deleting ${i}...` + ); + + if ( + existsSync( + path + ) + ) + rimraf.sync( + path + ); + } + }); + } + } else { + log.warning( + "Resetting does not work on manual patches that have a `delete` action, skipping..." + ); + } + } + ); + + let leftovers = new Set(); + + const { stdout: origFiles } = + await execa( + "git", + [ + "clean", + "-e", + "'!*.orig'", + "--dry-run" + ], + { cwd: ENGINE_DIR } + ); + + const { stdout: rejFiles } = + await execa( + "git", + [ + "clean", + "-e", + "'!*.rej'", + "--dry-run" + ], + { cwd: ENGINE_DIR } + ); + + origFiles + .split("\n") + .map((f) => + leftovers.add( + f.replace( + /Would remove /, + "" + ) + ) + ); + rejFiles + .split("\n") + .map((f) => + leftovers.add( + f.replace( + /Would remove /, + "" + ) + ) + ); + + Array.from(leftovers).forEach( + (f: any) => { + const path = resolve( + ENGINE_DIR, + f + ); + + if (path !== ENGINE_DIR) { + log.info( + `Deleting ${f}...` + ); + + rimraf.sync( + resolve(ENGINE_DIR, f) + ); + } + } + ); + + log.success("Reset successfully."); + log.info( + "Next time you build, it may need to recompile parts of the program because the cache was invalidated." + ); + } + }) + .catch((e) => e); + } catch (e) {} +}; diff --git a/src/commands/run.ts b/src/commands/run.ts new file mode 100644 index 0000000..6335106 --- /dev/null +++ b/src/commands/run.ts @@ -0,0 +1,36 @@ +import { existsSync, readdirSync } from "fs"; +import { resolve } from "path"; +import { bin_name, log } from ".."; +import { ENGINE_DIR } from "../constants"; +import { dispatch } from "../utils"; + +export const run = async (chrome?: string) => { + const dirs = readdirSync(ENGINE_DIR); + const objDirname: any = dirs.find((dir) => { + return dir.startsWith("obj-"); + }); + + if (!objDirname) { + throw new Error( + "Dot Browser needs to be built before you can do this." + ); + } + + const objDir = resolve(ENGINE_DIR, objDirname); + + if (existsSync(objDir)) { + dispatch( + "./mach", + ["run"].concat( + chrome ? ["-chrome", chrome] : [] + ), + ENGINE_DIR, + true, + true + ); + } else { + log.error( + `Unable to locate any built binaries.\nRun |${bin_name} build| to initiate a build.` + ); + } +}; diff --git a/src/commands/set-branch.ts b/src/commands/set-branch.ts new file mode 100644 index 0000000..a0301e8 --- /dev/null +++ b/src/commands/set-branch.ts @@ -0,0 +1,62 @@ +import execa from "execa"; +import { + existsSync, + readFileSync, + writeFileSync +} from "fs-extra"; +import { resolve } from "path"; +import { log } from ".."; + +export const setBranch = async (branch: string) => { + if ( + !existsSync( + resolve( + process.cwd(), + ".dotbuild", + "metadata" + ) + ) + ) { + return log.error( + "Cannot find metadata, aborting..." + ); + } + + const metadata = JSON.parse( + readFileSync( + resolve( + process.cwd(), + ".dotbuild", + "metadata" + ), + "utf-8" + ) + ); + + try { + await execa("git", [ + "rev-parse", + "--verify", + branch + ]); + + metadata.branch = branch; + + writeFileSync( + resolve( + process.cwd(), + ".dotbuild", + "metadata" + ), + JSON.stringify(metadata) + ); + + log.success( + `Default branch is at \`${branch}\`.` + ); + } catch (e) { + return log.error( + `Branch with name \`${branch}\` does not exist.` + ); + } +}; diff --git a/src/commands/status.ts b/src/commands/status.ts new file mode 100644 index 0000000..114c0db --- /dev/null +++ b/src/commands/status.ts @@ -0,0 +1,12 @@ +import { existsSync } from "fs"; +import { log } from ".."; +import { ENGINE_DIR } from "../constants"; +import { dispatch } from "../utils"; + +export const status = async () => { + if (existsSync(ENGINE_DIR)) { + dispatch("git", ["status"], ENGINE_DIR, true); + } else { + log.error(`Unable to locate src directory.`); + } +}; diff --git a/src/commands/test.ts b/src/commands/test.ts new file mode 100644 index 0000000..132e820 --- /dev/null +++ b/src/commands/test.ts @@ -0,0 +1,10 @@ +import { resolve } from "path"; +import { dispatch } from "../utils"; + +export const test = async () => { + dispatch( + "yarn", + ["test"], + resolve(process.cwd(), "src", "dot") + ); +}; diff --git a/src/constants/index.ts b/src/constants/index.ts new file mode 100644 index 0000000..ce8a452 --- /dev/null +++ b/src/constants/index.ts @@ -0,0 +1,51 @@ +import execa from "execa"; +import { resolve } from "path"; + +export const BUILD_TARGETS = [ + "linux", + "windows", + "macos" +]; + +export const ARCHITECTURE = ["i686", "x86_64"]; + +export const PATCH_ARGS = [ + "--ignore-space-change", + "--ignore-whitespace", + "--verbose" +]; + +export const ENGINE_DIR = resolve( + process.cwd(), + "engine" +); +export const SRC_DIR = resolve(process.cwd(), "src"); +export const PATCHES_DIR = resolve( + process.cwd(), + "patches" +); +export const COMMON_DIR = resolve( + process.cwd(), + "common" +); +export const CONFIGS_DIR = resolve( + process.cwd(), + "configs" +); + +export let CONFIG_GUESS: any = null; + +try { + CONFIG_GUESS = execa.commandSync( + "./build/autoconf/config.guess", + { cwd: ENGINE_DIR } + ).stdout; +} catch (e) {} + +export const OBJ_DIR = resolve( + ENGINE_DIR, + `obj-${CONFIG_GUESS}` +); + +export const FTL_STRING_LINE_REGEX = + /(([a-zA-Z0-9\-]*|\.[a-z\-]*) =(.*|\.)|\[[a-zA-Z0-9]*\].*(\n\s?\s?})?|\*\[[a-zA-Z0-9]*\] .*(\n\s?\s?})?)/gm; diff --git a/src/controllers/patch.ts b/src/controllers/patch.ts new file mode 100644 index 0000000..9c8f58c --- /dev/null +++ b/src/controllers/patch.ts @@ -0,0 +1,267 @@ +import chalk from "chalk"; +import execa from "execa"; +import { + existsSync, + rmdirSync, + rmSync, + statSync +} from "fs-extra"; +import { resolve } from "path"; +import readline from "readline"; +import { log } from ".."; +import { + ENGINE_DIR, + PATCH_ARGS, + SRC_DIR +} from "../constants"; +import { copyManual } from "../utils"; + +class Patch { + public name: string; + public action: string; + public src: string | string[]; + public type: "file" | "manual"; + public status: number[]; + public markers?: { + [key: string]: [string, string]; + }; + public indent?: number; + public options: { + minimal?: boolean; + noIgnore?: boolean; + }; + private _done: boolean = false; + + private error: Error | unknown; + + private async applyAsManual() { + return new Promise(async (res, rej) => { + try { + switch (this.action) { + case "copy": + if (typeof this.src == "string") { + copyManual( + this.src, + this.options.noIgnore + ); + } + + if (Array.isArray(this.src)) { + this.src.forEach((i) => { + copyManual( + i, + this.options.noIgnore + ); + }); + } + + break; + case "delete": + if (typeof this.src == "string") { + if ( + !existsSync( + resolve( + ENGINE_DIR, + this.src + ) + ) + ) + return log.error( + `We were unable to delete the file or directory \`${this.src}\` as it doesn't exist in the src directory.` + ); + + if ( + statSync( + resolve( + ENGINE_DIR, + this.src + ) + ).isDirectory() + ) { + rmdirSync( + resolve( + ENGINE_DIR, + this.src + ) + ); + } else { + rmSync( + resolve( + ENGINE_DIR, + this.src + ) + ); + } + } + + if (Array.isArray(this.src)) { + this.src.forEach((i) => { + if ( + !existsSync( + resolve( + ENGINE_DIR, + i + ) + ) + ) + return log.error( + `We were unable to delete the file or directory \`${i}\` as it doesn't exist in the src directory.` + ); + + if ( + statSync( + resolve( + ENGINE_DIR, + i + ) + ).isDirectory() + ) { + rmdirSync( + resolve( + ENGINE_DIR, + i + ) + ); + } else { + rmSync( + resolve( + ENGINE_DIR, + i + ), + { force: true } + ); + } + }); + } + + break; + } + + res(true); + } catch (e) { + rej(e); + } + }); + } + + private async applyAsPatch() { + return new Promise(async (res, rej) => { + try { + try { + await execa( + "git", + [ + "apply", + "-R", + ...PATCH_ARGS, + this.src as any + ], + { cwd: ENGINE_DIR } + ); + } catch (e) { + null; + } + + const { stdout, exitCode } = await execa( + "git", + [ + "apply", + ...PATCH_ARGS, + this.src as any + ], + { cwd: ENGINE_DIR } + ); + + if (exitCode == 0) res(true); + else throw stdout; + } catch (e) { + rej(e); + } + }); + } + + public async apply() { + if (!this.options.minimal) { + log.info( + `${chalk.gray( + `(${this.status[0]}/${this.status[1]})` + )} Applying ${this.name}...` + ); + } + + try { + if (this.type == "manual") + await this.applyAsManual(); + if (this.type == "file") + await this.applyAsPatch(); + + this.done = true; + } catch (e) { + this.error = e; + this.done = false; + } + } + + public get done() { + return this._done; + } + + public set done(_: any) { + this._done = _; + + if (!this.options.minimal) { + readline.moveCursor(process.stdout, 0, -1); + readline.clearLine(process.stdout, 1); + + log.info( + `${chalk.gray( + `(${this.status[0]}/${this.status[1]})` + )} Applying ${this.name}... ${chalk[ + this._done ? "green" : "red" + ].bold( + this._done ? "Done ✔" : "Error ❗" + )}` + ); + } + + if (this.error) { + throw this.error; + } + } + + constructor({ + name, + action, + src, + type, + status, + markers, + indent, + options + }: { + name: string; + action?: string; + src?: string | string[]; + type: "file" | "manual"; + status: number[]; + markers?: { + [key: string]: [string, string]; + }; + indent?: number; + options: { + minimal?: boolean; + noIgnore?: boolean; + }; + }) { + this.name = name; + this.action = action || ""; + this.src = src || resolve(SRC_DIR, name); + this.type = type; + this.status = status; + this.markers = markers; + this.indent = indent; + this.options = options; + } +} + +export default Patch; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..cfbced7 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,108 @@ +import chalk from "chalk"; +import commander, { Command } from "commander"; +import { existsSync, readFileSync } from "fs"; +import { resolve } from "path"; +import { commands } from "./cmds"; +import { ENGINE_DIR } from "./constants"; +import Log from "./log"; +import { shaCheck } from "./middleware/sha-check"; +import { updateCheck } from "./middleware/update-check"; +import { errorHandler } from "./utils"; + +const program = new Command(); + +export let log = new Log(); + +program + .storeOptionsAsProperties(false) + .passCommandToAction(false); + +const { dot, firefox, melon } = + require("../package.json").versions; + +let reportedFFVersion; + +if ( + existsSync( + resolve( + ENGINE_DIR, + "browser", + "config", + "version.txt" + ) + ) +) { + const version = readFileSync( + resolve( + ENGINE_DIR, + "browser", + "config", + "version.txt" + ), + "utf-8" + ).replace(/\n/g, ""); + + if (version !== firefox) reportedFFVersion = version; +} + +export const bin_name = "melon"; + +program.version(` +\t${chalk.bold("Dot Browser")} ${dot} +\t${chalk.bold("Firefox")} ${firefox} ${ + reportedFFVersion + ? `(being reported as ${reportedFFVersion})` + : `` +} +\t${chalk.bold("Melon")} ${melon} + +${ + reportedFFVersion + ? `Mismatch detected between expected Firefox version and the actual version. +You may have downloaded the source code using a different version and +then switched to another branch.` + : `` +} +`); +program.name(bin_name); + +commands.forEach((command) => { + if (command.flags) { + if ( + command.flags.platforms && + !command.flags.platforms.includes( + process.platform + ) + ) { + return; + } + } + + const _cmd = commander.command(command.cmd); + + _cmd.description(command.description); + + command?.aliases?.forEach((alias) => { + _cmd.alias(alias); + }); + + command?.options?.forEach((opt) => { + _cmd.option(opt.arg, opt.description); + }); + + _cmd.action(async (...args: any) => { + await shaCheck(command.cmd); + await updateCheck(); + + command.controller(...args); + }); + + program.addCommand(_cmd); +}); + +process.on("uncaughtException", errorHandler); +process.on("unhandledException", (err) => + errorHandler(err, true) +); + +program.parse(process.argv); diff --git a/src/interfaces/patch.ts b/src/interfaces/patch.ts new file mode 100644 index 0000000..b527f82 --- /dev/null +++ b/src/interfaces/patch.ts @@ -0,0 +1,9 @@ +export interface IPatch { + name: string; + action: string; + src: string | string[]; + markers?: { + [key: string]: [string, string]; + }; + indent?: number; +} diff --git a/src/log.ts b/src/log.ts new file mode 100644 index 0000000..5a5a646 --- /dev/null +++ b/src/log.ts @@ -0,0 +1,70 @@ +import chalk from "chalk"; + +class Log { + private startTime: number; + + constructor() { + const d = new Date(); + + this.startTime = d.getTime(); + } + + getDiff() { + const d = new Date(); + + const currentTime = d.getTime(); + + const elapsedTime = currentTime - this.startTime; + + var secs = Math.floor((elapsedTime / 1000) % 60); + var mins = Math.floor( + (elapsedTime / (60 * 1000)) % 60 + ); + var hours = Math.floor( + (elapsedTime / (60 * 60 * 1000)) % 24 + ); + + const format = (r: number) => { + return r.toString().length == 1 ? "0" + r : r; + }; + + return `${format(hours)}:${format(mins)}:${format( + secs + )}`; + } + + info(...args: any[]) { + console.info( + chalk.blueBright.bold(this.getDiff()), + ...args + ); + } + + warning(...args: any[]) { + console.info( + chalk.yellowBright.bold(" WARNING"), + ...args + ); + } + + hardWarning(...args: any[]) { + console.info( + "", + chalk.bgRed.bold("WARNING"), + ...args + ); + } + + success(...args: any[]) { + console.log( + `\n${chalk.greenBright.bold("SUCCESS")}`, + ...args + ); + } + + error(...args: any[]) { + throw new Error(...args); + } +} + +export default Log; diff --git a/src/manual-patches.ts b/src/manual-patches.ts new file mode 100644 index 0000000..81fb9e7 --- /dev/null +++ b/src/manual-patches.ts @@ -0,0 +1,32 @@ +import { sync } from "glob"; +import { SRC_DIR } from "./constants"; +import { IPatch } from "./interfaces/patch"; + +let files = sync("**/*", { + nodir: true, + cwd: SRC_DIR +}).filter( + (f) => + !( + f.endsWith(".patch") || + f.split("/").includes("node_modules") + ) +); + +const manualPatches: IPatch[] = []; + +files.map((i) => { + const group = i.split("/")[0]; + + if (!manualPatches.find((m) => m.name == group)) { + manualPatches.push({ + name: group, + action: "copy", + src: files.filter( + (f) => f.split("/")[0] == group + ) + }); + } +}); + +export default manualPatches; diff --git a/src/middleware/sha-check.ts b/src/middleware/sha-check.ts new file mode 100644 index 0000000..6f9b500 --- /dev/null +++ b/src/middleware/sha-check.ts @@ -0,0 +1,53 @@ +import execa from "execa"; +import { existsSync, readFileSync } from "fs-extra"; +import { resolve } from "path"; +import { bin_name, log } from ".."; + +const blacklistedCommands = [ + "reset", + "init", + "set-branch" +]; + +export const shaCheck = async (command: string) => { + if ( + blacklistedCommands.filter((c) => + command.startsWith(c) + ).length !== 0 || + !existsSync( + resolve( + process.cwd(), + ".dotbuild", + "metadata" + ) + ) + ) + return; + + const metadata = JSON.parse( + readFileSync( + resolve( + process.cwd(), + ".dotbuild", + "metadata" + ), + "utf-8" + ) + ); + + const { stdout: currentBranch } = await execa("git", [ + "branch", + "--show-current" + ]); + + if (metadata && metadata.branch) { + if (metadata.branch !== currentBranch) { + log.warning(`The current branch \`${currentBranch}\` differs from the original branch \`${metadata.branch}\`. + +\t If you are changing the Firefox version, you will need to reset the tree +\t with |${bin_name} reset --hard| and then |${bin_name} download|. + +\t Or you can change the default branch by typing |${bin_name} set-branch |.`); + } + } +}; diff --git a/src/middleware/update-check.ts b/src/middleware/update-check.ts new file mode 100644 index 0000000..6a64069 --- /dev/null +++ b/src/middleware/update-check.ts @@ -0,0 +1,31 @@ +import axios from "axios"; +import { log } from "../"; + +const pjson = require("../../package.json"); + +export const updateCheck = async () => { + const firefoxVersion = + pjson.versions["firefox-display"]; + + try { + const { data } = await axios.get( + `https://product-details.mozilla.org/1.0/firefox_history_major_releases.json`, + { timeout: 1000 } + ); + + if (data) { + let version = + Object.keys(data)[ + Object.keys(data).length - 1 + ]; + + if ( + firefoxVersion && + version !== firefoxVersion + ) + log.warning( + `Latest version of Firefox (${version}) does not match frozen version (${firefoxVersion}).` + ); + } + } catch (e) {} +}; diff --git a/src/types.d.ts b/src/types.d.ts new file mode 100644 index 0000000..897e458 --- /dev/null +++ b/src/types.d.ts @@ -0,0 +1,19 @@ +export interface Cmd { + cmd: string; + description: string; + + controller: (...args: any) => void; + + options?: CmdOption[]; + aliases?: string[]; + flags?: { + platforms?: CmdFlagPlatform[]; + }; +} + +export interface CmdOption { + arg: string; + description: string; +} + +export type CmdFlagPlatform = NodeJS.Platform; diff --git a/src/utils/delay.ts b/src/utils/delay.ts new file mode 100644 index 0000000..92ae0bb --- /dev/null +++ b/src/utils/delay.ts @@ -0,0 +1,5 @@ +export const delay = (delay: number) => { + return new Promise((resolve) => { + setTimeout(() => resolve(true), delay); + }); +}; diff --git a/src/utils/dispatch.ts b/src/utils/dispatch.ts new file mode 100644 index 0000000..779d310 --- /dev/null +++ b/src/utils/dispatch.ts @@ -0,0 +1,49 @@ +import execa from "execa"; +import { log } from ".."; + +const handle = (data: any, killOnError?: boolean) => { + const d = data.toString(); + + d.split("\n").forEach((line: any) => { + if (line.length !== 0) + log.info( + line.replace(/\s\d{1,5}:\d\d\.\d\d /g, "") + ); + }); + + if (killOnError) { + log.error("Command failed. See error above."); + process.exit(1); + } +}; + +export const dispatch = ( + cmd: string, + args?: any[], + cwd?: string, + noLog?: boolean, + killOnError?: boolean +) => { + return new Promise((resolve, reject) => { + process.env.MACH_USE_SYSTEM_PYTHON = "true"; + + const proc = execa(cmd, args, { + cwd: cwd ? cwd : process.cwd(), + env: process.env + }); + + proc.stdout?.on("data", (d) => handle(d)); + proc.stderr?.on("data", (d) => handle(d)); + + proc.stdout?.on("error", (d) => + handle(d, killOnError) + ); + proc.stderr?.on("error", (d) => + handle(d, killOnError) + ); + + proc.on("exit", () => { + resolve(true); + }); + }); +}; diff --git a/src/utils/error-handler.ts b/src/utils/error-handler.ts new file mode 100644 index 0000000..cac30c8 --- /dev/null +++ b/src/utils/error-handler.ts @@ -0,0 +1,45 @@ +import chalk from "chalk"; +import { readFileSync } from "fs-extra"; +import { resolve } from "path"; +import { log } from ".."; + +export const errorHandler = ( + err: Error, + isUnhandledRej: boolean +) => { + let cc = readFileSync( + resolve(process.cwd(), ".dotbuild", "command"), + "utf-8" + ); + cc = cc.replace(/(\r\n|\n|\r)/gm, ""); + + console.log( + `\n ${chalk.redBright.bold( + "ERROR" + )} An error occurred while running command ["${cc + .split(" ") + .join('", "')}"]:` + ); + console.log( + `\n\t`, + isUnhandledRej + ? err.toString().replace(/\n/g, "\n\t ") + : err.message.replace(/\n/g, "\n\t ") + ); + if (err.stack || isUnhandledRej) { + const stack: any = err.stack?.split("\n"); + stack.shift(); + stack.shift(); + console.log( + `\t`, + stack + .join("\n") + .replace(/(\r\n|\n|\r)/gm, "") + .replace(/ at /g, "\n\t • ") + ); + } + + console.log(); + log.info("Exiting due to error."); + process.exit(1); +}; diff --git a/src/utils/import.ts b/src/utils/import.ts new file mode 100644 index 0000000..8c40439 --- /dev/null +++ b/src/utils/import.ts @@ -0,0 +1,66 @@ +import { + appendFileSync, + ensureSymlink, + lstatSync, + readFileSync +} from "fs-extra"; +import { resolve } from "path"; +import rimraf from "rimraf"; +import { ENGINE_DIR, SRC_DIR } from "../constants"; + +const getChunked = (location: string) => { + return location.replace(/\\/g, "/").split("/"); +}; + +export const copyManual = ( + name: string, + noIgnore?: boolean +) => { + try { + try { + if ( + !lstatSync( + resolve( + ENGINE_DIR, + ...getChunked(name) + ) + ).isSymbolicLink() + ) { + rimraf.sync( + resolve( + ENGINE_DIR, + ...getChunked(name) + ) + ); + } + } catch (e) {} + + ensureSymlink( + resolve(SRC_DIR, ...getChunked(name)), + resolve(ENGINE_DIR, ...getChunked(name)) + ); + + if (!noIgnore) { + const gitignore = readFileSync( + resolve(ENGINE_DIR, ".gitignore"), + "utf-8" + ); + + if ( + !gitignore.includes( + getChunked(name).join("/") + ) + ) + appendFileSync( + resolve(ENGINE_DIR, ".gitignore"), + `\n${getChunked(name).join("/")}` + ); + } + + return; + } catch (e) { + console.log(e); + process.exit(0); + // return e; + } +}; diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..e9f2a5c --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,6 @@ +export * from "./delay"; +export * from "./dispatch"; +export * from "./error-handler"; +export * from "./import"; +export * from "./version"; +export * from "./write-metadata"; diff --git a/src/utils/version.ts b/src/utils/version.ts new file mode 100644 index 0000000..ed22ec1 --- /dev/null +++ b/src/utils/version.ts @@ -0,0 +1,11 @@ +import axios from "axios"; + +export const getLatestFF = async () => { + const { data } = await axios.get( + `https://product-details.mozilla.org/1.0/firefox_history_major_releases.json` + ); + + return Object.keys(data)[ + Object.keys(data).length - 1 + ]; +}; diff --git a/src/utils/write-metadata.ts b/src/utils/write-metadata.ts new file mode 100644 index 0000000..5c5c51c --- /dev/null +++ b/src/utils/write-metadata.ts @@ -0,0 +1,26 @@ +import execa from "execa"; +import { writeFileSync } from "fs-extra"; +import { resolve } from "path"; + +const pjson = require("../../package.json"); + +export const writeMetadata = async () => { + const { stdout: sha } = await execa("git", [ + "rev-parse", + "HEAD" + ]); + const { stdout: branch } = await execa("git", [ + "branch", + "--show-current" + ]); + + writeFileSync( + resolve(process.cwd(), ".dotbuild", "metadata"), + JSON.stringify({ + sha, + branch, + birth: Date.now(), + versions: pjson.versions + }) + ); +};