Build the Apple Container extension
Turns the empty scaffold into a working Nova extension for Apple Container and container-compose, in the shape of the Docker Suite extension. Sidebar (Scripts/Sidebar): - System, Containers, Images, Volumes and Networks sections - Containers grouped into compose projects via the com.docker.compose.project label container-compose stamps; project rows drive compose up/down/build against the matching workspace file - Lifecycle, shell, logs, inspect, browse and copy commands per resource - Apple Container publishes no event stream, so the sidebar polls while visible and only reloads a section when a fingerprint of its data changes, which keeps selection and expansion intact Hostnames (Scripts/Hostnames.js): - A container's hostname is <name>.<domain> and the domain lives in Apple Container's config.toml, which has no CLI setter, so the Hostname Domain preference reads and writes that file directly - Changing it offers to register the domain with macOS and restart the services; the write preserves the file's other tables and its mode Language support: - Dockerfile and Compose syntaxes backed by tree-sitter grammars built by Tools/build-syntaxes.sh, pinned to revisions that generate ABI 14 - The Compose syntax is named dockercompose because that is the languageId docker-language-server recognises compose files by - docker-language-server is downloaded on first use rather than bundled, at roughly 40 MB Icons are generated by Tools/make-icons.py so they can be regenerated rather than hand-maintained. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UnSBcR5Lywz5Fbj5FmVbfc
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
//
|
||||
// Language support for Dockerfiles and Compose files, backed by
|
||||
// docker-language-server (github.com/docker/docker-language-server).
|
||||
//
|
||||
// The binary is a single ~40 MB executable, which is too large to ship inside
|
||||
// the extension, so the managed mode downloads the pinned release into the
|
||||
// extension's global storage the first time it is needed.
|
||||
//
|
||||
|
||||
const { exec } = require("../Utilities/Process");
|
||||
const notify = require("../Utilities/Notify");
|
||||
|
||||
const RELEASE_URL = "https://github.com/docker/docker-language-server/releases/download";
|
||||
// docker-language-server decides what a document is from the languageId the
|
||||
// editor sends, and Nova sends the syntax name. Compose support only switches
|
||||
// on for "dockercompose", which is why the Compose syntax carries that name.
|
||||
const SYNTAXES = ["dockerfile", "dockercompose"];
|
||||
|
||||
class DockerLanguageClient {
|
||||
constructor() {
|
||||
this.client = null;
|
||||
this.disposables = new CompositeDisposable();
|
||||
|
||||
for (let key of ["apple_container.lsp.mode", "apple_container.lsp.path", "apple_container.lsp.version"]) {
|
||||
this.disposables.add(nova.config.onDidChange(key, () => this.restart()));
|
||||
}
|
||||
}
|
||||
|
||||
get mode() {
|
||||
return nova.config.get("apple_container.lsp.mode", "string") || "Managed Binary";
|
||||
}
|
||||
|
||||
get version() {
|
||||
return (nova.config.get("apple_container.lsp.version", "string") || "").trim() || "v0.20.1";
|
||||
}
|
||||
|
||||
async start() {
|
||||
if (this.mode === "Disabled") {
|
||||
return;
|
||||
}
|
||||
|
||||
let path = null;
|
||||
|
||||
try {
|
||||
path = this.mode === "Custom Path" ? this.customPath() : await this.managedPath();
|
||||
} catch (error) {
|
||||
notify.configFailure("Docker Language Server unavailable", error, "apple-container-lsp");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!path) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.stop();
|
||||
|
||||
try {
|
||||
this.client = new LanguageClient(
|
||||
"unsupervised.AppleContainer.dls",
|
||||
"Docker Language Server",
|
||||
{
|
||||
path: path,
|
||||
args: ["start", "--stdio"],
|
||||
type: "stdio"
|
||||
},
|
||||
{
|
||||
syntaxes: SYNTAXES
|
||||
}
|
||||
);
|
||||
|
||||
this.client.onDidStop((error) => {
|
||||
if (error) {
|
||||
notify.failure(
|
||||
"Docker Language Server stopped",
|
||||
error,
|
||||
"apple-container-lsp-stopped"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
this.client.start();
|
||||
nova.subscriptions.add(this.client);
|
||||
} catch (error) {
|
||||
notify.failure("Docker Language Server failed to launch", error, "apple-container-lsp");
|
||||
this.client = null;
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (!this.client) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.client.stop();
|
||||
nova.subscriptions.remove(this.client);
|
||||
this.client = null;
|
||||
}
|
||||
|
||||
async restart() {
|
||||
this.stop();
|
||||
await this.start();
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.stop();
|
||||
this.disposables.dispose();
|
||||
}
|
||||
|
||||
customPath() {
|
||||
let path = (nova.config.get("apple_container.lsp.path", "string") || "").trim();
|
||||
|
||||
if (!path) {
|
||||
throw new Error("Set the path to docker-language-server in the extension's preferences.");
|
||||
}
|
||||
|
||||
if (!nova.fs.access(path, nova.fs.X_OK)) {
|
||||
throw new Error(`${path} is not an executable file.`);
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
// Returns the managed binary, downloading it when it is missing.
|
||||
async managedPath() {
|
||||
let architecture = await this.architecture();
|
||||
let name = `docker-language-server-darwin-${architecture}-${this.version}`;
|
||||
let directory = nova.extension.globalStoragePath;
|
||||
let path = nova.path.join(directory, name);
|
||||
|
||||
if (nova.fs.access(path, nova.fs.X_OK)) {
|
||||
return path;
|
||||
}
|
||||
|
||||
if (!nova.fs.access(directory, nova.fs.F_OK)) {
|
||||
nova.fs.mkdir(directory);
|
||||
}
|
||||
|
||||
await this.download(`${RELEASE_URL}/${this.version}/${name}`, path);
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
async architecture() {
|
||||
let result = await exec("/usr/bin/uname", ["-m"]);
|
||||
return result.stdout.trim() === "x86_64" ? "amd64" : "arm64";
|
||||
}
|
||||
|
||||
async download(url, destination) {
|
||||
notify.info(
|
||||
"Downloading Docker Language Server",
|
||||
`${this.version} for Dockerfile and Compose support. This happens once.`,
|
||||
"apple-container-lsp-download"
|
||||
);
|
||||
|
||||
// A partial file would look like a working binary on the next launch,
|
||||
// so download beside the target and move it into place at the end.
|
||||
let temporary = `${destination}.download`;
|
||||
|
||||
try {
|
||||
await exec("/usr/bin/curl", [
|
||||
"--fail",
|
||||
"--location",
|
||||
"--silent",
|
||||
"--show-error",
|
||||
"--output",
|
||||
temporary,
|
||||
url
|
||||
]);
|
||||
await exec("/bin/chmod", ["+x", temporary]);
|
||||
nova.fs.move(temporary, destination);
|
||||
} catch (error) {
|
||||
try {
|
||||
nova.fs.remove(temporary);
|
||||
} catch (cleanupError) {
|
||||
// Nothing to clean up.
|
||||
}
|
||||
|
||||
throw new Error(`Could not download ${url}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { DockerLanguageClient };
|
||||
Reference in New Issue
Block a user