Skip to content

Commit c4aa4f1

Browse files
authored
vscode extension settings + devcontainer setup + devbox commands (#262)
## Summary This doesn't fully implement the behaviour we discussed. But it does update the python interpreter and go path for the user. The dev container setup is also possible via command palette (cmd + shift + P) in vscode. - Added settings to the extension - Added auto generating dev container config files - cleaned up the code for more readability - Added devbox commands (`devbox init | add | shell | install | run`) to the extension ## How was it tested? - Open vscode-extensions/ in vscode - press Run+debug - check the prompt for update settings from devbox extension - press cmd+shift+p and type devbox > choose "Devbox: Generate Dev Containers config files" - check the generated files in .devcontainer/
1 parent b0fa27c commit c4aa4f1

File tree

3 files changed

+324
-14
lines changed

3 files changed

+324
-14
lines changed

vscode-extension/package.json

+67-2
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,75 @@
1313
"Other"
1414
],
1515
"activationEvents": [
16-
"workspaceContains:./devbox.json"
16+
"onStartupFinished"
1717
],
1818
"main": "./out/extension.js",
19-
"contributes": {},
19+
"contributes": {
20+
"commands": [
21+
{
22+
"command": "devbox.setupDevContainer",
23+
"title": "Devbox: Generate Dev Containers config files"
24+
},
25+
{
26+
"command": "devbox.add",
27+
"title": "Devbox: Add - add packages to your devbox project"
28+
},
29+
{
30+
"command": "devbox.remove",
31+
"title": "Devbox: Remove - remove packages from your devbox project"
32+
},
33+
{
34+
"command": "devbox.run",
35+
"title": "Devbox: Run - execute scripts specified in devbox.json"
36+
},
37+
{
38+
"command": "devbox.shell",
39+
"title": "Devbox: Shell - Go to devbox shell in the terminal"
40+
},
41+
{
42+
"command": "devbox.init",
43+
"title": "Devbox: Init - Initiate a devbox project"
44+
}
45+
],
46+
"menus": {
47+
"commandPalette": [
48+
{
49+
"command": "devbox.setupDevContainer",
50+
"when": "devbox.configFileExists == true"
51+
},
52+
{
53+
"command": "devbox.add",
54+
"when": "devbox.configFileExists == true"
55+
},
56+
{
57+
"command": "devbox.remove",
58+
"when": "devbox.configFileExists == true"
59+
},
60+
{
61+
"command": "devbox.run",
62+
"when": "devbox.configFileExists == true"
63+
},
64+
{
65+
"command": "devbox.shell",
66+
"when": "devbox.configFileExists == true"
67+
},
68+
{
69+
"command": "devbox.init",
70+
"when": "devbox.configFileExists == false"
71+
}
72+
]
73+
},
74+
"configuration": {
75+
"title": "devbox",
76+
"properties": {
77+
"devbox.autoShellOnTerminal": {
78+
"type": "boolean",
79+
"default": true,
80+
"description": "Automatically run devbox shell when terminal is opened."
81+
}
82+
}
83+
}
84+
},
2085
"scripts": {
2186
"vscode:prepublish": "yarn run compile",
2287
"compile": "tsc -p ./",

vscode-extension/src/devcontainer.ts

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { workspace, window, Uri } from 'vscode';
2+
import { posix } from 'path';
3+
4+
export async function setupDevContainerFiles(cpuArch: String) {
5+
try {
6+
if (!workspace.workspaceFolders) {
7+
return window.showInformationMessage('No folder or workspace opened');
8+
}
9+
const workspaceUri = workspace.workspaceFolders[0].uri;
10+
const devcontainerUri = Uri.joinPath(workspaceUri, '.devcontainer/');
11+
// Parsing devbox.json data
12+
const devboxJson = await readDevboxJson(workspaceUri);
13+
// creating .devcontainer directory and its files
14+
await workspace.fs.createDirectory(devcontainerUri);
15+
const dockerfileContent = getDockerfileContent();
16+
await workspace.fs.writeFile(
17+
Uri.joinPath(devcontainerUri, 'Dockerfile'),
18+
Buffer.from(dockerfileContent, 'utf8')
19+
);
20+
21+
const devContainerJSON = getDevcontainerJSON(devboxJson, cpuArch);
22+
await workspace.fs.writeFile(
23+
Uri.joinPath(devcontainerUri, 'devcontainer.json'),
24+
Buffer.from(devContainerJSON, 'utf8')
25+
);
26+
} catch (error) {
27+
console.error('Error processing devbox.json - ', error);
28+
window.showErrorMessage('Error processing devbox.json');
29+
}
30+
}
31+
32+
export async function readDevboxJson(workspaceUri: Uri) {
33+
const fileUri = workspaceUri.with({ path: posix.join(workspaceUri.path, 'devbox.json') });
34+
const readData = await workspace.fs.readFile(fileUri);
35+
const readStr = Buffer.from(readData).toString('utf8');
36+
const devboxJsonData = JSON.parse(readStr);
37+
return devboxJsonData;
38+
39+
}
40+
41+
42+
43+
function getDockerfileContent(): String {
44+
return `
45+
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.245.2/containers/debian/.devcontainer/base.Dockerfile
46+
47+
# [Choice] Debian version (use bullseye on local arm64/Apple Silicon): bullseye, buster
48+
ARG VARIANT="buster"
49+
FROM mcr.microsoft.com/vscode/devcontainers/base:0-\${VARIANT}
50+
51+
# These dependencies are required by Nix.
52+
RUN apt update -y
53+
RUN apt -y install --no-install-recommends curl xz-utils
54+
55+
USER vscode
56+
57+
# Install nix
58+
ARG NIX_INSTALL_SCRIPT=https://nixos.org/nix/install
59+
RUN curl -fsSL \${NIX_INSTALL_SCRIPT} | sh -s -- --no-daemon
60+
ENV PATH /home/vscode/.nix-profile/bin:\${PATH}
61+
62+
# Install devbox
63+
RUN sudo mkdir /devbox && sudo chown vscode /devbox
64+
RUN curl -fsSL https://get.jetpack.io/devbox | bash -s -- -f
65+
66+
# Setup devbox environment
67+
COPY --chown=vscode ./devbox.json /devbox/devbox.json
68+
RUN devbox shell --config /devbox/devbox.json -- echo "Nix Store Populated"
69+
ENV PATH /devbox/.devbox/nix/profile/default/bin:\${PATH}
70+
ENTRYPOINT devbox shell
71+
`;
72+
}
73+
74+
function getDevcontainerJSON(devboxJson: any, cpuArch: String): String {
75+
76+
let devcontainerObject: any = {};
77+
devcontainerObject = {
78+
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
79+
// https://github.com/microsoft/vscode-dev-containers/tree/v0.245.2/containers/debian
80+
"name": "Devbox Remote Container",
81+
"build": {
82+
"dockerfile": "./Dockerfile",
83+
// Update 'VARIANT' to pick a Debian version: bullseye, buster
84+
// Use bullseye on local arm64/Apple Silicon.
85+
"args": {
86+
"VARIANT": cpuArch.trim() === "arm64" ? "bullseye" : "buster"
87+
}
88+
},
89+
"customizations": {
90+
"vscode": {
91+
"settings": {
92+
// Add custom vscode settings for remote environment here
93+
},
94+
"extensions": [
95+
// Add custom vscode extensions for remote environment here
96+
]
97+
}
98+
},
99+
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
100+
"remoteUser": "vscode"
101+
};
102+
103+
devboxJson["packages"].forEach((pkg: String) => {
104+
if (pkg.includes("python3")) {
105+
devcontainerObject.customizations.vscode.settings["python.defaultInterpreterPath"] = "/devbox/.devbox/nix/profile/default/bin/python3";
106+
devcontainerObject.customizations.vscode.extensions.push("ms-python.python");
107+
}
108+
if (pkg.includes("go_1_") || pkg === "go") {
109+
devcontainerObject.customizations.vscode.extensions.push("golang.go");
110+
}
111+
//TODO: add support for other common languages
112+
});
113+
114+
return JSON.stringify(devcontainerObject, null, 4);
115+
}

vscode-extension/src/extension.ts

+142-12
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,157 @@
11
// The module 'vscode' contains the VS Code extensibility API
2-
// Import the module and reference it with the alias vscode in your code below
3-
import * as vscode from 'vscode';
2+
import * as util from 'util';
3+
import * as cp from 'child_process';
4+
import { workspace, window, commands, Uri, ExtensionContext, QuickPickItem } from 'vscode';
5+
import { setupDevContainerFiles, readDevboxJson } from './devcontainer';
46

57
// This method is called when your extension is activated
68
// Your extension is activated the very first time the command is executed
7-
export function activate(context: vscode.ExtensionContext) {
8-
9-
// Use the console to output diagnostic information (console.log) and errors (console.error)
9+
export function activate(context: ExtensionContext) {
1010
// This line of code will only be executed once when your extension is activated
11-
vscode.window.onDidOpenTerminal(async (event) => {
12-
runDevboxShell();
11+
initialCheckDevboxJSON();
12+
// Creating file watchers to watch for events on devbox.json
13+
const fswatcher = workspace.createFileSystemWatcher("**/devbox.json", false, false, false);
14+
fswatcher.onDidDelete(e => commands.executeCommand('setContext', 'devbox.configFileExists', false));
15+
fswatcher.onDidCreate(e => commands.executeCommand('setContext', 'devbox.configFileExists', true));
16+
fswatcher.onDidChange(e => initialCheckDevboxJSON());
17+
18+
// Check for devbox.json when a new folder is opened
19+
workspace.onDidChangeWorkspaceFolders(async (e) => initialCheckDevboxJSON());
20+
21+
// run devbox shell when terminal is opened
22+
window.onDidOpenTerminal(async (e) => {
23+
if (workspace.getConfiguration("devbox").get("autoShellOnTerminal")) {
24+
await runInTerminal('devbox shell');
25+
}
26+
});
27+
28+
const devboxAdd = commands.registerCommand('devbox.add', async () => {
29+
const result = await window.showInputBox({
30+
value: '',
31+
placeHolder: 'Package to add to devbox. E.g., python39',
32+
});
33+
await runInTerminal(`devbox add ${result}`);
34+
});
35+
36+
const devboxRun = commands.registerCommand('devbox.run', async () => {
37+
const items = await getDevboxScripts();
38+
if (items.length > 0) {
39+
const result = await window.showQuickPick(items);
40+
await runInTerminal(`devbox run ${result}`);
41+
} else {
42+
window.showInformationMessage("No scripts found in devbox.json");
43+
}
44+
});
45+
46+
const devboxShell = commands.registerCommand('devbox.shell', async () => {
47+
// todo: add support for --config path to devbox.json
48+
await runInTerminal('devbox shell');
49+
});
50+
51+
const devboxRemove = commands.registerCommand('devbox.remove', async () => {
52+
const items = await getDevboxPackages();
53+
if (items.length > 0) {
54+
const result = await window.showQuickPick(items);
55+
await runInTerminal(`devbox rm ${result}`);
56+
} else {
57+
window.showInformationMessage("No packages found in devbox.json");
58+
}
59+
});
60+
61+
const devboxInit = commands.registerCommand('devbox.init', async () => {
62+
await runInTerminal('devbox init');
63+
commands.executeCommand('setContext', 'devbox.configFileExists', true);
1364
});
65+
66+
const setupDevcontainer = commands.registerCommand('devbox.setupDevContainer', async () => {
67+
const exec = util.promisify(cp.exec);
68+
// determining cpu architecture - needed for devcontainer dockerfile
69+
const { stdout, stderr } = await exec("uname -m");
70+
let cpuArch = stdout;
71+
if (stderr) {
72+
console.log(stderr);
73+
const response = await window.showErrorMessage(
74+
"Could not determine the CPU architecture type. Is your architecture type Apple M1/arm64?",
75+
"Yes",
76+
"No",
77+
);
78+
cpuArch = response === "Yes" ? "arm64" : "undefined";
79+
}
80+
await setupDevContainerFiles(cpuArch);
81+
82+
});
83+
84+
context.subscriptions.push(devboxAdd);
85+
context.subscriptions.push(devboxRun);
86+
context.subscriptions.push(devboxInit);
87+
context.subscriptions.push(devboxShell);
88+
context.subscriptions.push(devboxInstall);
89+
context.subscriptions.push(setupDevcontainer);
1490
}
1591

16-
async function runDevboxShell() {
92+
async function initialCheckDevboxJSON() {
93+
// check if there is a workspace folder open
94+
if (workspace.workspaceFolders) {
95+
const workspaceUri = workspace.workspaceFolders[0].uri;
96+
try {
97+
// check if the folder has devbox.json in it
98+
await workspace.fs.stat(Uri.joinPath(workspaceUri, "devbox.json"));
99+
// devbox.json exists setcontext for devbox commands to be available
100+
commands.executeCommand('setContext', 'devbox.configFileExists', true);
101+
102+
} catch (err) {
103+
console.log(err);
104+
// devbox.json does not exist
105+
commands.executeCommand('setContext', 'devbox.configFileExists', false);
106+
console.log("devbox.json does not exist");
107+
}
108+
}
109+
}
17110

18-
const result = await vscode.workspace.findFiles('devbox.json');
19-
if (result.length > 0) {
20-
vscode.commands.executeCommand('workbench.action.terminal.sendSequence', {
21-
'text': 'devbox shell \r\n'
111+
async function runInTerminal(cmd: string) {
112+
// ensure a terminal is open
113+
// This check has to exist since there is no way for extension to run code in
114+
// the terminal, unless a terminal session is already open.
115+
if ((<any>window).terminals.length === 0) {
116+
window.showErrorMessage('No active terminals. Re-run the command without closing the opened terminal.');
117+
window.createTerminal({ name: `Terminal` }).show();
118+
} else { // A terminal is open
119+
// run the given cmd in terminal
120+
await commands.executeCommand('workbench.action.terminal.sendSequence', {
121+
'text': `${cmd}\r\n`
22122
});
23123
}
24124
}
25125

126+
async function getDevboxScripts(): Promise<string[]> {
127+
try {
128+
if (!workspace.workspaceFolders) {
129+
window.showInformationMessage('No folder or workspace opened');
130+
return [];
131+
}
132+
const workspaceUri = workspace.workspaceFolders[0].uri;
133+
const devboxJson = await readDevboxJson(workspaceUri);
134+
return Object.keys(devboxJson['shell']['scripts']);
135+
} catch (error) {
136+
console.error('Error processing devbox.json - ', error);
137+
return [];
138+
}
139+
}
140+
141+
async function getDevboxPackages(): Promise<string[]> {
142+
try {
143+
if (!workspace.workspaceFolders) {
144+
window.showInformationMessage('No folder or workspace opened');
145+
return [];
146+
}
147+
const workspaceUri = workspace.workspaceFolders[0].uri;
148+
const devboxJson = await readDevboxJson(workspaceUri);
149+
return devboxJson['packages'];
150+
} catch (error) {
151+
console.error('Error processing devbox.json - ', error);
152+
return [];
153+
}
154+
}
155+
26156
// This method is called when your extension is deactivated
27157
export function deactivate() { }

0 commit comments

Comments
 (0)