Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 18 additions & 7 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -927,13 +927,20 @@ async function runInit(args: string[]): Promise<number> {
}
try {
const project = await createLocalProjectService().init(process.cwd(), repo);
// Create the local Dolt repo now (the "git init" moment), so the folder is
// a working versioned checkout before `deltix start` ever runs.
const { BinaryManager } = await import('../contexts/binary-manager');
await new VersioningLocalService({
homeDir: process.env.DELTIX_HOME ?? join(homedir(), '.deltix'),
binaryManager: new BinaryManager(),
}).initLocalRepo({ repo: project.config.repo, projectRoot: project.root });
// Create the local Dolt repo (the "git init" moment). If the Dolt binary
// can't be resolved yet (e.g. first-run download needs network), don't
// fail the bind — `deltix start` will initialize the repo then.
try {
const { BinaryManager } = await import('../contexts/binary-manager');
await new VersioningLocalService({
homeDir: process.env.DELTIX_HOME ?? join(homedir(), '.deltix'),
binaryManager: new BinaryManager(),
}).initLocalRepo({ repo: project.config.repo, projectRoot: project.root });
} catch (err) {
printInfo(
`Project bound, but the local Dolt engine wasn't created yet (${String(err)}). \`deltix start\` will initialize it.`,
);
}
printSuccess(`Initialized Deltix project in ${project.root}`, {
repo,
config: project.configPath,
Expand Down Expand Up @@ -1023,6 +1030,10 @@ async function runStart(args: string[]): Promise<number> {
const identity = await resolveServerIdentity(repoArg);
if (!identity) return 1;
try {
// Ensure the local Dolt repo exists (idempotent) before serving it, so
// `start` works even if `init` deferred repo creation.
const local = await newLocalService();
await local.initLocalRepo(identity);
const state = await createMysqlEmbeddedService().start(identity);
printSuccess(`Local Dolt SQL server started for ${identity.repo}`, {
host: '127.0.0.1',
Expand Down
31 changes: 22 additions & 9 deletions src/contexts/binary-manager/binary-manager.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,24 @@
import { createHash } from 'node:crypto';
import { createReadStream, existsSync } from 'node:fs';
import { chmod, copyFile, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { homedir, tmpdir } from 'node:os';
import { join } from 'node:path';
import { runCommand, whichBinary } from '../../acl/dolt-exec';
import { loadEnv } from '../../shared/env';
import { createGitHubReleaseDownloader, type DoltDownloader } from './download';

export const DOLT_VERSION = '2.3.1';

/** Host OSes with an official Dolt release we know how to fetch. */
type DoltOs = 'darwin' | 'linux' | 'win32';

export interface BinaryManagerDeps {
/** Root state dir; defaults to `~/.deltix` (or `DELTIX_HOME`). */
homeDir?: string;
/** Overrides `DELTIX_DOLT_BIN_PATH` (and the env var). */
explicitBinPath?: string;
/** Overrides OS auto-detection (test/CI). */
os?: 'darwin' | 'linux';
os?: DoltOs;
/** Overrides arch auto-detection (test/CI). */
arch?: 'arm64' | 'amd64';
downloader?: DoltDownloader;
Expand Down Expand Up @@ -84,7 +87,8 @@ export class BinaryManager {

/** Absolute path to the install'd dolt executable for a version. */
binaryPath(version: string): string {
return join(this.versionDir(version), 'bin', 'dolt');
const exe = process.platform === 'win32' ? 'dolt.exe' : 'dolt';
return join(this.versionDir(version), 'bin', exe);
}

/** Returns the installed binary path if present and digest-verified. */
Expand Down Expand Up @@ -121,11 +125,12 @@ export class BinaryManager {
);
try {
const stagedBin = await downloader.download(url, stageDir);
const dest = join(binDir, 'dolt');
const exe = process.platform === 'win32' ? 'dolt.exe' : 'dolt';
const dest = join(binDir, exe);
const tmpDest = join(binDir, `.dolt.tmp-${Math.random().toString(36).slice(2)}`);
await copyFile(stagedBin, tmpDest);
try {
await chmod(tmpDest, 0o755);
await chmod(tmpDest, 0o755).catch(() => {});
await rename(tmpDest, dest);
} catch (err) {
await rm(tmpDest, { force: true });
Expand All @@ -141,16 +146,21 @@ export class BinaryManager {
}
}

function defaultOs(): 'darwin' | 'linux' {
return process.platform === 'darwin' ? 'darwin' : 'linux';
function defaultOs(): DoltOs {
if (process.platform === 'darwin') return 'darwin';
if (process.platform === 'win32') return 'win32';
return 'linux';
}

function defaultArch(): 'arm64' | 'amd64' {
return process.arch === 'arm64' ? 'arm64' : 'amd64';
}

function defaultHomeDir(): string {
return join(process.env.HOME ?? '', '.deltix');
// Use os.homedir() — on Windows `process.env.HOME` is typically undefined
// (the platform uses USERPROFILE), which previously produced a *relative*
// `.deltix/...` path and made the resolved binary unfindable.
return join(homedir(), '.deltix');
}

async function findOnPath(version: string): Promise<string | null> {
Expand Down Expand Up @@ -179,7 +189,10 @@ function sha256File(path: string): Promise<string> {
}

export function doltReleaseUrl(version: string, os: string, arch: string): string {
const platform = os === 'darwin' ? 'darwin' : 'linux';
const a = arch === 'arm64' ? 'arm64' : 'amd64';
if (os === 'win32') {
return `https://github.com/dolthub/dolt/releases/download/v${version}/dolt-windows-${a}.zip`;
}
const platform = os === 'darwin' ? 'darwin' : 'linux';
return `https://github.com/dolthub/dolt/releases/download/v${version}/dolt-${platform}-${a}.tar.gz`;
}
17 changes: 9 additions & 8 deletions src/contexts/binary-manager/download.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { Readable } from 'node:stream';
import { pipeline } from 'node:stream/promises';
import { extractTarGz, findDoltExecutable } from './tar-extract';
import { extractArchive, findDoltExecutable } from './tar-extract';

export interface DoltDownloader {
/**
Expand All @@ -39,23 +39,24 @@ export function createGitHubReleaseDownloader(): DoltDownloader {
throw new Error('Download of Dolt failed: empty response body');
}

const tarballPath = join(
const isZip = url.endsWith('.zip');
const archivePath = join(
tmpdir(),
`deltix-dolt-${process.pid}-${Math.random().toString(36).slice(2)}.tar.gz`,
`deltix-dolt-${process.pid}-${Math.random().toString(36).slice(2)}.${isZip ? 'zip' : 'tar.gz'}`,
);
await mkdir(destDir, { recursive: true });

try {
await pipeline(Readable.fromWeb(response.body as never), createWriteStream(tarballPath));
await extractTarGz(tarballPath, destDir);
await pipeline(Readable.fromWeb(response.body as never), createWriteStream(archivePath));
await extractArchive(archivePath, destDir);
const binary = await findDoltExecutable(destDir);
if (!binary) {
throw new Error('Downloaded Dolt tarball contained no dolt executable');
throw new Error('Downloaded Dolt archive contained no dolt executable');
}
await chmod(binary, 0o755);
await chmod(binary, 0o755).catch(() => {});
return binary;
} finally {
await rm(tarballPath, { force: true }).catch(() => {});
await rm(archivePath, { force: true }).catch(() => {});
}
},
};
Expand Down
45 changes: 22 additions & 23 deletions src/contexts/binary-manager/tar-extract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,46 +13,45 @@ import { join } from 'node:path';
import { runCommand, whichBinary } from '../../acl/dolt-exec';

/**
* Extracts the gzip'd tarball at `tarballPath` into `destDir` using the
* platform `tar`. Throws when tar is unavailable or the extraction fails.
* Extracts the Dolt release archive (`.tar.gz` on unix, `.zip` on Windows)
* into `destDir` using the platform `tar`. `-xf` lets the tool auto-detect the
* format: GNU tar handles the gzip tarball on Linux/macOS; the bsdtar shipped
* with Windows 10+ also expands the `.zip`. Throws when tar is missing or
* extraction fails.
*/
export async function extractTarGz(tarballPath: string, destDir: string): Promise<void> {
export async function extractArchive(archivePath: string, destDir: string): Promise<void> {
await mkdir(destDir, { recursive: true });
const tar = await whichBinary('tar');
if (!tar) {
throw new Error('No `tar` binary available to extract the Dolt archive');
}
const result = await runCommand(tar, ['-xzf', tarballPath, '-C', destDir]);
const result = await runCommand(tar, ['-xf', archivePath, '-C', destDir]);
if (result.exitCode !== 0) {
throw new Error(`Failed to extract Dolt tarball: ${result.stderr.trim() || 'tar error'}`);
throw new Error(`Failed to extract Dolt archive: ${result.stderr.trim() || 'tar error'}`);
}
}

/** Walks `root` and returns the path of a `dolt` executable if found. */
export async function findDoltExecutable(root: string): Promise<string | null> {
// Common layout of official release tarballs: `dolt/bin/dolt` (darwin) or
// `dolt-linux-amd64/bin/dolt`. Prefer well-known paths, then fall back to
// a recursive scan so we stay robust to future layout changes.
const expected = [
join(root, 'dolt', 'bin', 'dolt'),
join(
root,
`dolt-${process.platform === 'darwin' ? 'darwin' : 'linux'}${archSuffix()}`,
'bin',
'dolt',
),
];
const exe = process.platform === 'win32' ? 'dolt.exe' : 'dolt';
const dirName = archiveDirName();
// Common layouts: `dolt/bin/dolt`, `dolt-<os>-<arch>/bin/dolt`, and the
// Windows zip's `dolt-windows-amd64\bin\dolt.exe`. Prefer known paths, then
// fall back to a recursive scan so we stay robust to layout changes.
const expected = [join(root, 'dolt', 'bin', exe), join(root, dirName, 'bin', exe)];
for (const candidate of expected) {
if (existsSync(candidate)) return candidate;
}
return (await scanForDolt(root)) ?? null;
return (await scanForDolt(root, exe)) ?? null;
}

function archSuffix(): string {
return process.arch === 'arm64' ? '-arm64' : '-amd64';
function archiveDirName(): string {
const os =
process.platform === 'darwin' ? 'darwin' : process.platform === 'win32' ? 'windows' : 'linux';
return `dolt-${os}${process.arch === 'arm64' ? '-arm64' : '-amd64'}`;
}

async function scanForDolt(dir: string): Promise<string | null> {
async function scanForDolt(dir: string, exe: string): Promise<string | null> {
let entries: Awaited<ReturnType<typeof readdir>> | undefined;
try {
entries = await readdir(dir, { withFileTypes: true });
Expand All @@ -62,9 +61,9 @@ async function scanForDolt(dir: string): Promise<string | null> {
for (const entry of entries) {
const full = join(dir, entry.name);
if (entry.isDirectory()) {
const found = await scanForDolt(full);
const found = await scanForDolt(full, exe);
if (found) return found;
} else if (entry.isFile() && entry.name === 'dolt') {
} else if (entry.isFile() && entry.name === exe) {
return full;
}
}
Expand Down