Skip to content

Commit

Permalink
feat: add support for running inside a container
Browse files Browse the repository at this point in the history
The cli can now work properly inside a container if the `DOCKER_CONTAINER` environment variable is
set. A devcontainer is set up for this repository both as an example and as a development
environment for the cli and libdragon itself.
  • Loading branch information
anacierdem committed Nov 13, 2022
1 parent 1ad27f8 commit ac9c80b
Show file tree
Hide file tree
Showing 16 changed files with 240 additions and 49 deletions.
9 changes: 9 additions & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# syntax=docker/dockerfile:1
FROM ghcr.io/dragonminded/libdragon:latest

ENV DOCKER_CONTAINER=true

COPY ./.devcontainer/init.sh /tmp/init.sh

WORKDIR /tmp
RUN /bin/bash -c ./init.sh
9 changes: 9 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
// Use image instead of build, if you don't need the cli installed
// "image": ghcr.io/dragonminded/libdragon:latest
"build": { "dockerfile": "./Dockerfile", "context": "../" },
"workspaceMount": "source=${localWorkspaceFolder},target=/libdragon,type=bind",
"workspaceFolder": "/libdragon",
// You can execute ./build.sh instead, if you don't need the cli in the container
"postCreateCommand": "npm install && npm link && libdragon install"
}
9 changes: 9 additions & 0 deletions .devcontainer/init.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'

# Install nodejs & npm
apt-get update
apt install curl -y
curl -fsSL https://deb.nodesource.com/setup_16.x | bash -
apt-get install nodejs
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* text=auto eol=lf
2 changes: 1 addition & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[submodule "libdragon"]
[submodule "libdragon"]
path = libdragon
url = https://github.com/DragonMinded/libdragon
branch = trunk
25 changes: 24 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ For a quick development loop it really helps linking the code in this repository

npm link

in the root of the repository. Once you do this, running `libdragon` will use the code here rather than the actual npm installation. Then you can test your changes in the libdragon project here or elsewhere on your computer.
in the root of the repository. Once you do this, running `libdragon` will use the code here rather than the actual npm installation. Then you can test your changes in the libdragon project here or elsewhere on your computer. This setup is automatically done if you use the [devcontainer](#experimental-devcontainer-support).

When you are happy with your changes, you can verify you conform to the coding standards via:

Expand All @@ -201,6 +201,29 @@ This repository uses [`semantic-release`](https://github.com/semantic-release/se

It will create a `semantic-release` compatible commit from your current staged changes.

### Experimental devcontainer support

The repository provides a configuration (in `.devcontainer`) so that IDEs that support it can create and run the Docker container for you. Then, you can start working on it as if you are working on a machine with libdragon installed.

With the provided setup, you can continue using the cli in the container and it will work for non-container specific actions like `install`, `disasm` etc. You don't have to use the cli in the container, but you can. In general it will be easier and faster to just run `make` in the container but this setup is included to ease developing the cli as well.

To create your own dev container backed project, you can use the contents of the `.devcontainer` folder as reference. You don't need to include nodejs or the cli and you can just run `build.sh` as `postCreateCommand`. See the `devcontainer.json` for more details. As long as your container have the `DOCKER_CONTAINER` environment variable, the tool can work inside a container.

#### Caveats

- In the devcontainer, uploading via USB will not work.
- Error matching is not yet tested.
- Ideally the necessary extensions should be automatically installed. This is not configured yet.

<details>
<summary>vscode instructions</summary>

- Make sure you have the [Dev container extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) installed and you fulfill its [requirements](https://code.visualstudio.com/docs/devcontainers/containers).
- Clone this repository with `--recurse-submodules` or run `git submodule update --init`.
- Open command palette and run `Dev Containers: Reopen in container`.
- It will prepare the container and open it in the editor.
</details>

## As an NPM dependency

You can install libdragon as an NPM dependency by `npm install libdragon --save` in order to use docker in your N64 projects. A `libdragon` command similar to global intallation is provided that can be used in your NPM scripts as follows;
Expand Down
8 changes: 7 additions & 1 deletion modules/actions/destroy.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,19 @@ const path = require('path');

const { destroyContainer } = require('./utils');
const { CONFIG_FILE, LIBDRAGON_PROJECT_MANIFEST } = require('../constants');
const { fileExists, dirExists, log } = require('../helpers');
const { fileExists, dirExists, log, ValidationError } = require('../helpers');
const chalk = require('chalk');

/**
* @param {import('../project-info').LibdragonInfo} libdragonInfo
*/
const destroy = async (libdragonInfo) => {
if (process.env.DOCKER_CONTAINER) {
throw new ValidationError(
`Not possible to destroy the container from inside.`
);
}

await destroyContainer(libdragonInfo);

const projectPath = path.join(libdragonInfo.root, LIBDRAGON_PROJECT_MANIFEST);
Expand Down
18 changes: 18 additions & 0 deletions modules/actions/exec.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const {
fileExists,
dirExists,
CommandError,
spawnProcess,
} = require('../helpers');

const { start } = require('./start');
Expand Down Expand Up @@ -45,6 +46,23 @@ const exec = async (info) => {
true
);

// Don't even bother here, we are already in a container.
if (process.env.DOCKER_CONTAINER) {
const enableTTY = Boolean(process.stdout.isTTY && process.stdin.isTTY);
await spawnProcess(info.options.EXTRA_PARAMS[0], parameters, {
userCommand: true,
// Inherit stdin/out in tandem if we are going to disable TTY o/w the input
// stream remains inherited by the node process while the output pipe is
// waiting data from stdout and it behaves like we are still controlling
// the spawned process while the terminal is actually displaying say for
// example `less`.
inheritStdout: enableTTY,
inheritStdin: enableTTY,
inheritStderr: true,
});
return info;
}

const stdin = new PassThrough();

/** @type {string[]} */
Expand Down
43 changes: 26 additions & 17 deletions modules/actions/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,11 @@ const autoVendor = async (info) => {
return info;
}

await runGitMaybeHost(info, ['init']);
// Container re-init breaks file modes assume there is git for this case.
// TODO: we should remove the unnecessary inits in the future.
if (!process.env.DOCKER_CONTAINER) {
await runGitMaybeHost(info, ['init']);
}

// TODO: TS thinks this is already defined
const detectedStrategy = await autoDetect(info);
Expand Down Expand Up @@ -220,28 +224,33 @@ async function init(info) {
LIBDRAGON_PROJECT_MANIFEST
)} exists. This is already a libdragon project, starting it...`
);
if (info.options.DOCKER_IMAGE) {
info = await syncImageAndStart(info);
} else {
info = {
...info,
containerId: await start(info),
};
if (!process.env.DOCKER_CONTAINER) {
if (info.options.DOCKER_IMAGE) {
info = await syncImageAndStart(info);
} else {
info = {
...info,
containerId: await start(info),
};
}
}

info = await autoVendor(info);
await installDependencies(info);
return info;
}

await updateImage(info, info.imageName);

// Download image and start it
info.containerId = await start(info);

// We have created a new container, save the new info ASAP
await initGitAndCacheContainerId(
/** @type Parameters<initGitAndCacheContainerId>[0] */ (info)
);
if (!process.env.DOCKER_CONTAINER) {
// Download image and start it
await updateImage(info, info.imageName);
info.containerId = await start(info);
// We have created a new container, save the new info ASAP
// When in a container, we should already have git
// Re-initing breaks file modes anyways
await initGitAndCacheContainerId(
/** @type Parameters<initGitAndCacheContainerId>[0] */ (info)
);
}

info = await autoVendor(info);

Expand Down
33 changes: 18 additions & 15 deletions modules/actions/install.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,24 @@ const { log } = require('../helpers');
*/
const install = async (libdragonInfo) => {
let updatedInfo = libdragonInfo;
const imageName = libdragonInfo.options.DOCKER_IMAGE;
// If an image is provided, attempt to install
if (imageName) {
log(
chalk.yellow(
'Using `install` action to update the docker image is deprecated. Use the `update` action instead.'
)
);
updatedInfo = await syncImageAndStart(libdragonInfo);
} else {
// Make sure existing one is running
updatedInfo = {
...updatedInfo,
containerId: await start(libdragonInfo),
};

if (!process.env.DOCKER_CONTAINER) {
const imageName = libdragonInfo.options.DOCKER_IMAGE;
// If an image is provided, attempt to install
if (imageName) {
log(
chalk.yellow(
'Using `install` action to update the docker image is deprecated. Use the `update` action instead.'
)
);
updatedInfo = await syncImageAndStart(libdragonInfo);
} else {
// Make sure existing one is running
updatedInfo = {
...updatedInfo,
containerId: await start(libdragonInfo),
};
}
}

// Re-install vendors on new image
Expand Down
23 changes: 22 additions & 1 deletion modules/actions/start.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
const chalk = require('chalk').stderr;

const { CONTAINER_TARGET_PATH } = require('../constants');
const { spawnProcess, log, print, dockerExec } = require('../helpers');
const {
spawnProcess,
log,
print,
dockerExec,
assert,
ValidationError,
} = require('../helpers');

const {
checkContainerAndClean,
Expand All @@ -14,6 +21,11 @@ const {
* @param {import('../project-info').LibdragonInfo} libdragonInfo
*/
const initContainer = async (libdragonInfo) => {
assert(
!process.env.DOCKER_CONTAINER,
new Error('initContainer does not make sense in a container')
);

let newId;
try {
log('Creating new container...');
Expand Down Expand Up @@ -79,6 +91,11 @@ const initContainer = async (libdragonInfo) => {
* @param {import('../project-info').LibdragonInfo} libdragonInfo
*/
const start = async (libdragonInfo) => {
assert(
!process.env.DOCKER_CONTAINER,
new Error('Cannot start a container when we are already in a container.')
);

const running =
libdragonInfo.containerId &&
(await checkContainerRunning(libdragonInfo.containerId));
Expand Down Expand Up @@ -108,6 +125,10 @@ module.exports = /** @type {const} */ ({
* @param {import('../project-info').LibdragonInfo} libdragonInfo
*/
fn: async (libdragonInfo) => {
if (process.env.DOCKER_CONTAINER) {
throw new ValidationError(`We are already in a container.`);
}

const containerId = await start(libdragonInfo);
print(containerId);
return { ...libdragonInfo, containerId };
Expand Down
8 changes: 7 additions & 1 deletion modules/actions/stop.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { spawnProcess } = require('../helpers');
const { spawnProcess, ValidationError } = require('../helpers');

const { checkContainerRunning } = require('./utils');

Expand All @@ -7,6 +7,12 @@ const { checkContainerRunning } = require('./utils');
* @returns {Promise<import('../project-info').LibdragonInfo | void>}
*/
const stop = async (libdragonInfo) => {
if (process.env.DOCKER_CONTAINER) {
throw new ValidationError(
`Not possible to stop the container from inside.`
);
}

const running =
libdragonInfo.containerId &&
(await checkContainerRunning(libdragonInfo.containerId));
Expand Down
9 changes: 8 additions & 1 deletion modules/actions/update-and-start.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
const { log } = require('../helpers');
const { log, assert } = require('../helpers');
const { updateImage, destroyContainer } = require('./utils');
const { start } = require('./start');

/**
* @param {import('../project-info').LibdragonInfo} libdragonInfo
*/
async function syncImageAndStart(libdragonInfo) {
assert(
!process.env.DOCKER_CONTAINER,
new Error(
'[syncImageAndStart] We should already know we are in a container.'
)
);

const oldImageName = libdragonInfo.imageName;
const imageName = libdragonInfo.options.DOCKER_IMAGE ?? oldImageName;
// If an image is provided, always attempt to install it
Expand Down
Loading

0 comments on commit ac9c80b

Please sign in to comment.