copy tea/cli install strategy from tea/cli repo (#561)

- new tea cli install setup
- reinstall if uninstalled/rm by user 
- initialize state can be cancelled
---------

Co-authored-by: Max Howell <mxcl@me.com>
Co-authored-by: neil molina <neil@neils-MacBook-Pro.local>
This commit is contained in:
Neil 2023-05-05 14:21:20 +08:00 committed by GitHub
parent 2287982310
commit b7e45f0768
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 153 additions and 68 deletions

View file

@ -1,16 +1,14 @@
import { spawn, exec } from "child_process";
import { getGuiPath } from "./tea-dir";
import { getGuiPath, getTeaPath } from "./tea-dir";
import fs from "fs";
import path from "path";
import initializeTeaCli from "./initialize";
import { initializeTeaCli } from "./initialize";
import { app } from "electron";
import log from "./logger";
import { MainWindowNotifier } from "./types";
const destinationDirectory = getGuiPath();
export const cliBinPath = path.join(destinationDirectory, "tea");
export const cliBinPath = path.join(getTeaPath(), "tea.xyz/v*/bin/tea");
export async function installPackage(
full_name: string,
@ -34,7 +32,7 @@ export async function installPackage(
const opts = { env: { HOME: app.getPath("home"), NO_COLOR: "1" } };
const child = spawn(
`${destinationDirectory}/tea`,
cliBinPath,
["--env=false", "--sync", "--json", `+${qualifedPackage}`],
opts
);
@ -200,6 +198,6 @@ export async function syncPantry() {
const teaVersion = await initializeTeaCli();
if (!teaVersion) throw new Error("no tea");
log.info("Syncing pantry");
await asyncExec(`cd '${destinationDirectory}' && ./tea -S`);
log.info("Syncing pantry", teaVersion);
await asyncExec(`DEBUG=1 "${cliBinPath}" --sync --env=false`);
}

View file

@ -1,78 +1,162 @@
import fs from "fs";
import { getGuiPath } from "./tea-dir";
import { getGuiPath, getTeaPath } from "./tea-dir";
import log from "./logger";
import { cliBinPath, asyncExec } from "./cli";
// import { cliBinPath, asyncExec } from "./cli";
import { createInitialSessionFile } from "./auth";
import { SemVer, isValidSemVer } from "@tea/libtea";
import * as https from "https";
import { spawn } from "child_process";
import path from "path";
import { parse as semverParse } from "@tea/libtea";
const MINIMUM_TEA_VERSION = "0.31.2";
type InitState = "NOT_INITIALIZED" | "PENDING" | "INITIALIZED";
const destinationDirectory = getGuiPath();
class InitWatcher<T> {
private initState: InitState;
private initFunction: () => Promise<T>;
private initialValue: T | undefined;
private initializationPromise: Promise<T> | undefined;
// TODO: move device_id generation here
// Get the binary path from the current app directory
const binaryUrl = "https://tea.xyz/$(uname)/$(uname -m)";
let initializePromise: Promise<string> | null = null;
export async function initializeTeaCli(): Promise<string> {
if (initializePromise) {
return initializePromise;
constructor(initFunction: () => Promise<T>) {
this.initState = "NOT_INITIALIZED";
this.initFunction = initFunction;
this.initialValue = undefined;
this.initializationPromise = undefined;
}
log.info("Initializing tea cli");
initializePromise = initializeTeaCliInternal();
async initialize(): Promise<T> {
if (this.initState === "NOT_INITIALIZED") {
this.initState = "PENDING";
this.initializationPromise = this.retryFunction(this.initFunction, 3)
.then((value) => {
this.initialValue = value;
this.initState = "INITIALIZED";
return value;
})
.catch((error) => {
this.initState = "NOT_INITIALIZED";
this.initializationPromise = undefined;
throw error;
});
}
initializePromise.catch((error) => {
log.info("Error initializing tea cli, resetting promise:", error);
initializePromise = null;
});
return initializePromise;
}
async function initializeTeaCliInternal(): Promise<string> {
let binCheck = "";
let needsUpdate = false;
// Create the destination directory if it doesn't exist
if (!fs.existsSync(destinationDirectory)) {
fs.mkdirSync(destinationDirectory, { recursive: true });
return this.initializationPromise as Promise<T>;
}
// replace this with max's pr
const curlCommand = `curl --insecure -L -o "${cliBinPath}" "${binaryUrl}"`;
const exists = fs.existsSync(cliBinPath);
if (exists) {
log.info("binary tea already exists at", cliBinPath);
async retryFunction(func: () => Promise<T>, retries: number, currentAttempt = 1): Promise<T> {
try {
binCheck = await asyncExec(`cd ${destinationDirectory} && ./tea --version`);
const teaVersion = binCheck.toString().split(" ")[1].trim();
if (new SemVer(teaVersion).compare(new SemVer(MINIMUM_TEA_VERSION)) < 0) {
log.info("binary tea version is too old, updating");
needsUpdate = true;
}
const result = await func();
return result;
} catch (error) {
// probably binary is not executable or no permission
log.error("Error checking tea binary version:", error);
needsUpdate = true;
await asyncExec(`cd ${destinationDirectory} && rm tea`);
if (currentAttempt < retries) {
return this.retryFunction(func, retries, currentAttempt + 1);
} else {
throw error;
}
}
}
if (!exists || needsUpdate) {
await asyncExec(curlCommand);
log.info("Binary downloaded and saved to", cliBinPath);
await asyncExec("chmod u+x " + cliBinPath);
log.info("Binary is now ready for use at", cliBinPath);
binCheck = await asyncExec(`cd ${destinationDirectory} && ./tea --version`);
reset(): void {
this.initState = "NOT_INITIALIZED";
this.initializationPromise = undefined;
}
const version = binCheck.toString().split(" ")[1];
log.info("binary tea version:", version);
return isValidSemVer(version.trim()) ? version : "";
async observe(): Promise<T> {
return await this.initialize();
}
getState(): InitState {
return this.initState;
}
}
const teaCliPrefix = path.join(getTeaPath(), "tea.xyz/v*");
export const cliInitializationState = new InitWatcher<string>(async () => {
if (!fs.existsSync(path.join(teaCliPrefix, "bin/tea"))) {
return installTeaCli();
} else {
const dir = fs.readlinkSync(teaCliPrefix);
const v = semverParse(dir)?.toString();
if (!v) throw new Error(`couldn't parse to semver: ${dir}`);
return v;
}
});
cliInitializationState.initialize();
export async function initializeTeaCli(): Promise<string> {
if (
cliInitializationState.getState() === "INITIALIZED" &&
!fs.existsSync(path.join(teaCliPrefix, "bin/tea"))
) {
cliInitializationState.reset();
}
return cliInitializationState.observe();
}
//NOTE copy pasta from https://github.com/teaxyz/setup/blob/main/action.js
//FIXME ideally we'd not copy pasta this
//NOTE using `tar` is not ideal ∵ Windows and even though tar is POSIX it's still not guaranteed to be available
async function installTeaCli() {
const PREFIX = `${process.env.HOME}/.tea`;
const midfix = (() => {
switch (process.arch) {
case "arm64":
return `${process.platform}/aarch64`;
case "x64":
return `${process.platform}/x86-64`;
default:
throw new Error(`unsupported platform: ${process.platform}/${process.arch}`);
}
})();
/// versions.txt is guaranteed semver-sorted
const v: string | undefined = await new Promise((resolve, reject) => {
https
.get(`https://dist.tea.xyz/tea.xyz/${midfix}/versions.txt`, (rsp) => {
if (rsp.statusCode != 200) return reject(rsp.statusCode);
rsp.setEncoding("utf8");
const chunks: string[] = [];
rsp.on("data", (x) => chunks.push(x));
rsp.on("end", () => {
resolve(chunks.join("").trim().split("\n").at(-1));
});
})
.on("error", reject);
});
if (!v) throw new Error(`invalid versions.txt for tea/cli`);
fs.mkdirSync(PREFIX, { recursive: true });
const exitcode = await new Promise((resolve, reject) => {
https
.get(`https://dist.tea.xyz/tea.xyz/${midfix}/v${v}.tar.gz`, (rsp) => {
if (rsp.statusCode != 200) return reject(rsp.statusCode);
const tar = spawn("tar", ["xzf", "-"], {
stdio: ["pipe", "inherit", "inherit"],
cwd: PREFIX
});
rsp.pipe(tar.stdin);
tar.on("close", resolve);
})
.on("error", reject);
});
if (exitcode != 0) {
throw new Error(`tar: ${exitcode}`);
}
const oldwd = process.cwd();
process.chdir(`${PREFIX}/tea.xyz`);
if (fs.existsSync(`v*`)) fs.unlinkSync(`v*`);
fs.symlinkSync(`v${v}`, `v*`, "dir");
if (fs.existsSync(`v0`)) fs.unlinkSync(`v0`);
fs.symlinkSync(`v${v}`, `v0`, "dir"); //FIXME
process.chdir(oldwd);
return v;
}
export default async function initialize(): Promise<string> {

View file

@ -6,7 +6,7 @@ import log from "./logger";
import { syncLogsAt } from "./v1-client";
import { installPackage, openPackageEntrypointInTerminal, syncPantry } from "./cli";
import initializeTeaCli from "./initialize";
import { initializeTeaCli, cliInitializationState } from "./initialize";
import { getAutoUpdateStatus, getUpdater } from "./auto-updater";
@ -142,7 +142,10 @@ export default function initializeHandlers({ notifyMainWindow }: HandlerOptions)
await deletePackageFolder(fullName, version);
} catch (e) {
log.error(e);
return e;
} finally {
if (fullName === "tea.xyz") {
cliInitializationState.reset();
}
}
}
);

View file

@ -1 +1 @@
export { default as SemVer, isValidSemVer } from "./semver";
export { default as SemVer, isValidSemVer, parse } from "./semver";