libdragon 10.8.2 → 10.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -184,7 +184,7 @@ For a quick development loop it really helps linking the code in this repository
184
184
 
185
185
  npm link
186
186
 
187
- 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.
187
+ 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).
188
188
 
189
189
  When you are happy with your changes, you can verify you conform to the coding standards via:
190
190
 
@@ -201,6 +201,29 @@ This repository uses [`semantic-release`](https://github.com/semantic-release/se
201
201
 
202
202
  It will create a `semantic-release` compatible commit from your current staged changes.
203
203
 
204
+ ### Experimental devcontainer support
205
+
206
+ 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.
207
+
208
+ 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.
209
+
210
+ 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.
211
+
212
+ #### Caveats
213
+
214
+ - In the devcontainer, uploading via USB will not work.
215
+ - Error matching is not yet tested.
216
+ - Ideally the necessary extensions should be automatically installed. This is not configured yet.
217
+
218
+ <details>
219
+ <summary>vscode instructions</summary>
220
+
221
+ - 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).
222
+ - Clone this repository with `--recurse-submodules` or run `git submodule update --init`.
223
+ - Open command palette and run `Dev Containers: Reopen in container`.
224
+ - It will prepare the container and open it in the editor.
225
+ </details>
226
+
204
227
  ## As an NPM dependency
205
228
 
206
229
  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;
@@ -3,13 +3,19 @@ const path = require('path');
3
3
 
4
4
  const { destroyContainer } = require('./utils');
5
5
  const { CONFIG_FILE, LIBDRAGON_PROJECT_MANIFEST } = require('../constants');
6
- const { fileExists, dirExists, log } = require('../helpers');
6
+ const { fileExists, dirExists, log, ValidationError } = require('../helpers');
7
7
  const chalk = require('chalk');
8
8
 
9
9
  /**
10
10
  * @param {import('../project-info').LibdragonInfo} libdragonInfo
11
11
  */
12
12
  const destroy = async (libdragonInfo) => {
13
+ if (process.env.DOCKER_CONTAINER) {
14
+ throw new ValidationError(
15
+ `Not possible to destroy the container from inside.`
16
+ );
17
+ }
18
+
13
19
  await destroyContainer(libdragonInfo);
14
20
 
15
21
  const projectPath = path.join(libdragonInfo.root, LIBDRAGON_PROJECT_MANIFEST);
@@ -9,6 +9,7 @@ const {
9
9
  fileExists,
10
10
  dirExists,
11
11
  CommandError,
12
+ spawnProcess,
12
13
  } = require('../helpers');
13
14
 
14
15
  const { start } = require('./start');
@@ -45,6 +46,23 @@ const exec = async (info) => {
45
46
  true
46
47
  );
47
48
 
49
+ // Don't even bother here, we are already in a container.
50
+ if (process.env.DOCKER_CONTAINER) {
51
+ const enableTTY = Boolean(process.stdout.isTTY && process.stdin.isTTY);
52
+ await spawnProcess(info.options.EXTRA_PARAMS[0], parameters, {
53
+ userCommand: true,
54
+ // Inherit stdin/out in tandem if we are going to disable TTY o/w the input
55
+ // stream remains inherited by the node process while the output pipe is
56
+ // waiting data from stdout and it behaves like we are still controlling
57
+ // the spawned process while the terminal is actually displaying say for
58
+ // example `less`.
59
+ inheritStdout: enableTTY,
60
+ inheritStdin: enableTTY,
61
+ inheritStderr: true,
62
+ });
63
+ return info;
64
+ }
65
+
48
66
  const stdin = new PassThrough();
49
67
 
50
68
  /** @type {string[]} */
@@ -108,7 +108,11 @@ const autoVendor = async (info) => {
108
108
  return info;
109
109
  }
110
110
 
111
- await runGitMaybeHost(info, ['init']);
111
+ // Container re-init breaks file modes assume there is git for this case.
112
+ // TODO: we should remove the unnecessary inits in the future.
113
+ if (!process.env.DOCKER_CONTAINER) {
114
+ await runGitMaybeHost(info, ['init']);
115
+ }
112
116
 
113
117
  // TODO: TS thinks this is already defined
114
118
  const detectedStrategy = await autoDetect(info);
@@ -220,33 +224,33 @@ async function init(info) {
220
224
  LIBDRAGON_PROJECT_MANIFEST
221
225
  )} exists. This is already a libdragon project, starting it...`
222
226
  );
223
- if (info.options.DOCKER_IMAGE) {
224
- log(
225
- `Not changing docker image. Use the install action if you want to override the image.`
226
- );
227
- }
228
- if (info.options.DOCKER_IMAGE) {
229
- info = await syncImageAndStart(info);
230
- } else {
231
- info = {
232
- ...info,
233
- containerId: await start(info),
234
- };
227
+ if (!process.env.DOCKER_CONTAINER) {
228
+ if (info.options.DOCKER_IMAGE) {
229
+ info = await syncImageAndStart(info);
230
+ } else {
231
+ info = {
232
+ ...info,
233
+ containerId: await start(info),
234
+ };
235
+ }
235
236
  }
237
+
236
238
  info = await autoVendor(info);
237
239
  await installDependencies(info);
238
240
  return info;
239
241
  }
240
242
 
241
- await updateImage(info, info.imageName);
242
-
243
- // Download image and start it
244
- info.containerId = await start(info);
245
-
246
- // We have created a new container, save the new info ASAP
247
- await initGitAndCacheContainerId(
248
- /** @type Parameters<initGitAndCacheContainerId>[0] */ (info)
249
- );
243
+ if (!process.env.DOCKER_CONTAINER) {
244
+ // Download image and start it
245
+ await updateImage(info, info.imageName);
246
+ info.containerId = await start(info);
247
+ // We have created a new container, save the new info ASAP
248
+ // When in a container, we should already have git
249
+ // Re-initing breaks file modes anyways
250
+ await initGitAndCacheContainerId(
251
+ /** @type Parameters<initGitAndCacheContainerId>[0] */ (info)
252
+ );
253
+ }
250
254
 
251
255
  info = await autoVendor(info);
252
256
 
@@ -274,7 +278,7 @@ module.exports = /** @type {const} */ ({
274
278
 
275
279
  By default, a git repository and a submodule at \`./libdragon\` will be created to automatically update the vendored libdragon files on subsequent \`update\`s. If you intend to opt-out from this feature, see the \`--strategy manual\` flag to provide your self-managed libdragon copy. The default behaviour is intended for users who primarily want to consume libdragon as is.
276
280
 
277
- If this is the first time you are creating a libdragon project at that location, this action will also create skeleton project files to kickstart things with the given image, if provided. For subsequent runs, it will act like \`start\` thus can be used to revive an existing project without modifying it.
281
+ If this is the first time you are creating a libdragon project at that location, this action will also create skeleton project files to kickstart things with the given image, if provided. For subsequent runs without any parameter, it will act like \`start\` thus can be used to revive an existing project without modifying it.
278
282
 
279
283
  If you have an existing project with an already vendored submodule or subtree libdragon copy, \`init\` will automatically detect it at the provided \`--directory\`.`,
280
284
  group: ['docker', 'vendoring'],
@@ -13,21 +13,24 @@ const { log } = require('../helpers');
13
13
  */
14
14
  const install = async (libdragonInfo) => {
15
15
  let updatedInfo = libdragonInfo;
16
- const imageName = libdragonInfo.options.DOCKER_IMAGE;
17
- // If an image is provided, attempt to install
18
- if (imageName) {
19
- log(
20
- chalk.yellow(
21
- 'Using `install` action to update the docker image is deprecated. Use the `update` action instead.'
22
- )
23
- );
24
- updatedInfo = await syncImageAndStart(libdragonInfo);
25
- } else {
26
- // Make sure existing one is running
27
- updatedInfo = {
28
- ...updatedInfo,
29
- containerId: await start(libdragonInfo),
30
- };
16
+
17
+ if (!process.env.DOCKER_CONTAINER) {
18
+ const imageName = libdragonInfo.options.DOCKER_IMAGE;
19
+ // If an image is provided, attempt to install
20
+ if (imageName) {
21
+ log(
22
+ chalk.yellow(
23
+ 'Using `install` action to update the docker image is deprecated. Use the `update` action instead.'
24
+ )
25
+ );
26
+ updatedInfo = await syncImageAndStart(libdragonInfo);
27
+ } else {
28
+ // Make sure existing one is running
29
+ updatedInfo = {
30
+ ...updatedInfo,
31
+ containerId: await start(libdragonInfo),
32
+ };
33
+ }
31
34
  }
32
35
 
33
36
  // Re-install vendors on new image
@@ -1,7 +1,14 @@
1
1
  const chalk = require('chalk').stderr;
2
2
 
3
3
  const { CONTAINER_TARGET_PATH } = require('../constants');
4
- const { spawnProcess, log, print, dockerExec } = require('../helpers');
4
+ const {
5
+ spawnProcess,
6
+ log,
7
+ print,
8
+ dockerExec,
9
+ assert,
10
+ ValidationError,
11
+ } = require('../helpers');
5
12
 
6
13
  const {
7
14
  checkContainerAndClean,
@@ -14,6 +21,11 @@ const {
14
21
  * @param {import('../project-info').LibdragonInfo} libdragonInfo
15
22
  */
16
23
  const initContainer = async (libdragonInfo) => {
24
+ assert(
25
+ !process.env.DOCKER_CONTAINER,
26
+ new Error('initContainer does not make sense in a container')
27
+ );
28
+
17
29
  let newId;
18
30
  try {
19
31
  log('Creating new container...');
@@ -79,6 +91,11 @@ const initContainer = async (libdragonInfo) => {
79
91
  * @param {import('../project-info').LibdragonInfo} libdragonInfo
80
92
  */
81
93
  const start = async (libdragonInfo) => {
94
+ assert(
95
+ !process.env.DOCKER_CONTAINER,
96
+ new Error('Cannot start a container when we are already in a container.')
97
+ );
98
+
82
99
  const running =
83
100
  libdragonInfo.containerId &&
84
101
  (await checkContainerRunning(libdragonInfo.containerId));
@@ -108,6 +125,10 @@ module.exports = /** @type {const} */ ({
108
125
  * @param {import('../project-info').LibdragonInfo} libdragonInfo
109
126
  */
110
127
  fn: async (libdragonInfo) => {
128
+ if (process.env.DOCKER_CONTAINER) {
129
+ throw new ValidationError(`We are already in a container.`);
130
+ }
131
+
111
132
  const containerId = await start(libdragonInfo);
112
133
  print(containerId);
113
134
  return { ...libdragonInfo, containerId };
@@ -1,4 +1,4 @@
1
- const { spawnProcess } = require('../helpers');
1
+ const { spawnProcess, ValidationError } = require('../helpers');
2
2
 
3
3
  const { checkContainerRunning } = require('./utils');
4
4
 
@@ -7,6 +7,12 @@ const { checkContainerRunning } = require('./utils');
7
7
  * @returns {Promise<import('../project-info').LibdragonInfo | void>}
8
8
  */
9
9
  const stop = async (libdragonInfo) => {
10
+ if (process.env.DOCKER_CONTAINER) {
11
+ throw new ValidationError(
12
+ `Not possible to stop the container from inside.`
13
+ );
14
+ }
15
+
10
16
  const running =
11
17
  libdragonInfo.containerId &&
12
18
  (await checkContainerRunning(libdragonInfo.containerId));
@@ -1,4 +1,4 @@
1
- const { log } = require('../helpers');
1
+ const { log, assert } = require('../helpers');
2
2
  const { updateImage, destroyContainer } = require('./utils');
3
3
  const { start } = require('./start');
4
4
 
@@ -6,6 +6,13 @@ const { start } = require('./start');
6
6
  * @param {import('../project-info').LibdragonInfo} libdragonInfo
7
7
  */
8
8
  async function syncImageAndStart(libdragonInfo) {
9
+ assert(
10
+ !process.env.DOCKER_CONTAINER,
11
+ new Error(
12
+ '[syncImageAndStart] We should already know we are in a container.'
13
+ )
14
+ );
15
+
9
16
  const oldImageName = libdragonInfo.imageName;
10
17
  const imageName = libdragonInfo.options.DOCKER_IMAGE ?? oldImageName;
11
18
  // If an image is provided, always attempt to install it
@@ -53,11 +53,16 @@ const installDependencies = async (libdragonInfo) => {
53
53
 
54
54
  /**
55
55
  * Downloads the given docker image. Returns false if the local image is the
56
- * same, new image name otherwise.
56
+ * same, true otherwise.
57
57
  * @param {import('../project-info').LibdragonInfo} libdragonInfo
58
58
  * @param {string} newImageName
59
59
  */
60
60
  const updateImage = async (libdragonInfo, newImageName) => {
61
+ assert(
62
+ !process.env.DOCKER_CONTAINER,
63
+ new Error('[updateImage] should not be called in a container')
64
+ );
65
+
61
66
  // Will not take too much time if already have the same
62
67
  const download = async () => {
63
68
  log(`Downloading docker image: ${newImageName}`);
@@ -89,13 +94,18 @@ const updateImage = async (libdragonInfo, newImageName) => {
89
94
  }
90
95
 
91
96
  log(`Image is different: ${newImageName}`, true);
92
- return newImageName;
97
+ return true;
93
98
  };
94
99
 
95
100
  /**
96
101
  * @param {import('../project-info').LibdragonInfo} libdragonInfo
97
102
  */
98
103
  const destroyContainer = async (libdragonInfo) => {
104
+ assert(
105
+ !process.env.DOCKER_CONTAINER,
106
+ new Error('[destroyContainer] should not be called in a container')
107
+ );
108
+
99
109
  if (libdragonInfo.containerId) {
100
110
  await spawnProcess('docker', [
101
111
  'container',
@@ -144,6 +154,11 @@ async function runGitMaybeHost(libdragonInfo, params, options = {}) {
144
154
  throw e;
145
155
  }
146
156
 
157
+ assert(
158
+ !process.env.DOCKER_CONTAINER,
159
+ new Error('[runGitMaybeHost] Native git should exist in a container.')
160
+ );
161
+
147
162
  return await dockerExec(
148
163
  libdragonInfo,
149
164
  // Use the host user when initializing git as we will need access
@@ -159,6 +174,13 @@ async function runGitMaybeHost(libdragonInfo, params, options = {}) {
159
174
  * @param {import('../project-info').LibdragonInfo} libdragonInfo
160
175
  */
161
176
  async function checkContainerAndClean(libdragonInfo) {
177
+ assert(
178
+ !process.env.DOCKER_CONTAINER,
179
+ new Error(
180
+ '[checkContainerAndClean] We should already know we are in a container.'
181
+ )
182
+ );
183
+
162
184
  const id =
163
185
  libdragonInfo.containerId &&
164
186
  (
@@ -188,6 +210,13 @@ async function checkContainerAndClean(libdragonInfo) {
188
210
  * @param {string} containerId
189
211
  */
190
212
  async function checkContainerRunning(containerId) {
213
+ assert(
214
+ !process.env.DOCKER_CONTAINER,
215
+ new Error(
216
+ '[checkContainerRunning] We should already know we are in a container.'
217
+ )
218
+ );
219
+
191
220
  const running = (
192
221
  await spawnProcess('docker', [
193
222
  'container',
@@ -203,11 +232,16 @@ async function checkContainerRunning(containerId) {
203
232
  * @param {import('../project-info').LibdragonInfo & {containerId: string}} libdragonInfo
204
233
  */
205
234
  async function initGitAndCacheContainerId(libdragonInfo) {
235
+ if (!libdragonInfo.containerId) {
236
+ return;
237
+ }
238
+
206
239
  // If there is managed vendoring, make sure we have a git repo. `git init` is
207
240
  // safe anyways...
208
241
  if (libdragonInfo.vendorStrategy !== 'manual') {
209
242
  await runGitMaybeHost(libdragonInfo, ['init']);
210
243
  }
244
+
211
245
  const gitFolder = path.join(libdragonInfo.root, '.git');
212
246
  if (await dirExists(gitFolder)) {
213
247
  await fs.writeFile(
@@ -157,6 +157,10 @@ function spawnProcess(
157
157
  spawnOptions: {},
158
158
  }
159
159
  ) {
160
+ assert(
161
+ cmd !== 'docker' || !process.env.DOCKER_CONTAINER,
162
+ new Error('Trying to invoke docker inside a container.')
163
+ );
160
164
  return new Promise((resolve, reject) => {
161
165
  /** @type {Buffer[]} */
162
166
  const stdout = [];
@@ -273,11 +277,6 @@ const dockerExec = /** @type {DockerExec} */ (
273
277
  /** @type {SpawnOptions | undefined} */
274
278
  options
275
279
  ) {
276
- assert(
277
- !!libdragonInfo.containerId,
278
- new Error('Trying to invoke dockerExec without a containerId.')
279
- );
280
-
281
280
  // TODO: assert for invalid args
282
281
  const haveDockerParams =
283
282
  Array.isArray(dockerParams) && Array.isArray(cmdWithParams);
@@ -286,6 +285,35 @@ const dockerExec = /** @type {DockerExec} */ (
286
285
  options = /** @type {SpawnOptions} */ (cmdWithParams);
287
286
  }
288
287
 
288
+ const finalCmdWithParams = haveDockerParams ? cmdWithParams : dockerParams;
289
+ const finalDockerParams = haveDockerParams ? dockerParams : [];
290
+
291
+ assert(
292
+ finalDockerParams.findIndex(
293
+ (val) => val === '--workdir=' || val === '-w='
294
+ ) === -1,
295
+ new Error('Do not use `=` syntax when setting working dir')
296
+ );
297
+
298
+ // Convert docker execs into regular commands in the correct cwd
299
+ if (process.env.DOCKER_CONTAINER) {
300
+ const workDirIndex = finalDockerParams.findIndex(
301
+ (val) => val === '--workdir' || val === '-w'
302
+ );
303
+ const workDir =
304
+ workDirIndex >= 0 ? finalDockerParams[workDirIndex + 1] : undefined;
305
+ return spawnProcess(finalCmdWithParams[0], finalCmdWithParams.slice(1), {
306
+ ...options,
307
+ spawnOptions: { cwd: workDir, ...options?.spawnOptions },
308
+ });
309
+ }
310
+
311
+ assert(
312
+ !!libdragonInfo.containerId,
313
+ new Error('Trying to invoke dockerExec without a containerId.')
314
+ );
315
+
316
+ /** @type string[] */
289
317
  const additionalParams = [];
290
318
 
291
319
  // Docker TTY wants in & out streams both to be a TTY
@@ -309,11 +337,10 @@ const dockerExec = /** @type {DockerExec} */ (
309
337
  'docker',
310
338
  [
311
339
  'exec',
312
- ...(haveDockerParams
313
- ? [...dockerParams, ...additionalParams]
314
- : additionalParams),
340
+ ...finalDockerParams,
341
+ ...additionalParams,
315
342
  libdragonInfo.containerId,
316
- ...(haveDockerParams ? cmdWithParams : dockerParams),
343
+ ...finalCmdWithParams,
317
344
  ],
318
345
  options
319
346
  );
@@ -64,6 +64,11 @@ const {
64
64
  * @param {LibdragonInfo} libdragonInfo
65
65
  */
66
66
  async function findContainerId(libdragonInfo) {
67
+ assert(
68
+ !process.env.DOCKER_CONTAINER,
69
+ new Error('[findContainerId] We should already know we are in a container.')
70
+ );
71
+
67
72
  const idFile = path.join(libdragonInfo.root, '.git', CACHED_CONTAINER_FILE);
68
73
  if (await fileExists(idFile)) {
69
74
  const id = (await fs.readFile(idFile, { encoding: 'utf8' })).trim();
@@ -225,15 +230,19 @@ const readProjectInfo = async function (optionInfo) {
225
230
  }
226
231
  }
227
232
 
228
- info.containerId = await findContainerId(info);
229
- log(`Active container id: ${info.containerId}`, true);
233
+ if (!process.env.DOCKER_CONTAINER) {
234
+ info.containerId = await findContainerId(info);
235
+ log(`Active container id: ${info.containerId}`, true);
236
+ }
230
237
 
231
238
  // For imageName, flag has the highest priority followed by the one read from
232
239
  // the file and then if there is any matching container, name is read from it.
233
240
  // As last option fallback to default value.
234
241
 
235
242
  // If still have the container, read the image name from it
243
+ // No need to do anything if we are in a container
236
244
  if (
245
+ !process.env.DOCKER_CONTAINER &&
237
246
  !info.imageName &&
238
247
  info.containerId &&
239
248
  (await checkContainerAndClean(info))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libdragon",
3
- "version": "10.8.2",
3
+ "version": "10.9.0",
4
4
  "description": "This is a docker wrapper for libdragon",
5
5
  "main": "index.js",
6
6
  "engines": {