libdragon 10.8.1 → 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
@@ -1,6 +1,6 @@
1
1
  # Docker Libdragon
2
2
 
3
- [![Build](https://github.com/anacierdem/libdragon-docker/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/anacierdem/libdragon-docker/actions/workflows/ci.yml)
3
+ [![Build](https://github.com/anacierdem/libdragon-docker/actions/workflows/release.yml/badge.svg?branch=master)](https://github.com/anacierdem/libdragon-docker/actions/workflows/release.yml)
4
4
 
5
5
  This is a wrapper for a docker container to make managing the libdragon toolchain easier. It has the additional advantage that libdragon toolchain and library can be installed on a per-project basis instead of managing system-wide installations.
6
6
 
@@ -184,14 +184,14 @@ 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
 
191
191
  npm run format-check
192
192
  npm run lint-check
193
193
 
194
- You can auto-fix applicable errors by running `format` and `lint` scripts instead. Additionally, typescript is used as the type system. To be able to get away with transpiling the code during development, jsDoc flavor of types are used instead of inline ones. To check if your types, run:
194
+ You can auto-fix applicable errors by running `format` and `lint` scripts instead. Additionally, typescript is used as the type system. To be able to get away with transpiling the code during development, jsDoc flavor of types are used instead of inline ones. To check your types, run:
195
195
 
196
196
  npm run tsc
197
197
 
@@ -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;
@@ -242,11 +265,6 @@ will init the container for this project and run `make && make install` for `ed6
242
265
 
243
266
  This is an experimental dependency management.
244
267
 
245
- ## TODOS
246
-
247
- - [ ] Skip CI checks for irrelevant changes.
248
- - [ ] Verify the NPM dependency mechanism is still working and add a test.
249
-
250
268
  ## Funding
251
269
 
252
270
  If this tool helped you, consider supporting its development by sponsoring it!
package/index.js CHANGED
@@ -18,9 +18,30 @@ const {
18
18
  const { parseParameters } = require('./modules/parameters');
19
19
  const { readProjectInfo, writeProjectInfo } = require('./modules/project-info');
20
20
 
21
+ // Note: it is not possible to merge these type definitions in a single comment
22
+ /**
23
+ * @template {any} [U=any]
24
+ * @typedef {(U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never} UnionToIntersection
25
+ */
26
+ /**
27
+ * @template {any} [K=any]
28
+ * never does not break the call `info.options.CURRENT_ACTION.fn`
29
+ * @typedef {[K] extends [UnionToIntersection<K>] ? any : unknown} NoUnion
30
+ * @typedef {NoUnion<Exclude<Parameters<import('./modules/parameters').Actions[import('./modules/project-info').ActionsNoProject]['fn']>[0], undefined>>} EitherCLIOrLibdragonInfo
31
+ */
32
+
21
33
  parseParameters(process.argv)
22
34
  .then(readProjectInfo)
23
- .then((info) => info.options.CURRENT_ACTION.fn(info))
35
+ .then((info) => {
36
+ return info.options.CURRENT_ACTION.fn(
37
+ /** @type {EitherCLIOrLibdragonInfo} */ (info)
38
+ );
39
+ // This type make sure a similar restriction to this code block is enforced
40
+ // without adding unnecessary javascript.
41
+ // return isProjectAction(info)
42
+ // ? info.options.CURRENT_ACTION.fn(info)
43
+ // : info.options.CURRENT_ACTION.fn(info);
44
+ })
24
45
  .catch((e) => {
25
46
  if (e instanceof ParameterError) {
26
47
  log(chalk.red(e.message));
@@ -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);
@@ -13,7 +13,7 @@ const {
13
13
  /**
14
14
  * @param {string} stop
15
15
  * @param {string} start
16
- * @return {Promise<string>}
16
+ * @returns {Promise<string>}
17
17
  */
18
18
  const findElf = async (stop, start = '.') => {
19
19
  start = path.resolve(start);
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  *
3
3
  * @param {import('../project-info').LibdragonInfo} libdragonInfo
4
- * @returns
5
4
  */
6
5
  function dockerHostUserParams(libdragonInfo) {
7
6
  const { uid, gid } = libdragonInfo.userInfo;
@@ -8,6 +8,8 @@ const {
8
8
  toPosixPath,
9
9
  fileExists,
10
10
  dirExists,
11
+ CommandError,
12
+ spawnProcess,
11
13
  } = require('../helpers');
12
14
 
13
15
  const { start } = require('./start');
@@ -16,7 +18,6 @@ const { installDependencies } = require('./utils');
16
18
 
17
19
  /**
18
20
  * @param {import('../project-info').LibdragonInfo} libdragonInfo
19
- * @returns
20
21
  */
21
22
  function dockerRelativeWorkdir(libdragonInfo) {
22
23
  return (
@@ -27,18 +28,14 @@ function dockerRelativeWorkdir(libdragonInfo) {
27
28
  }
28
29
 
29
30
  /**
30
- *
31
31
  * @param {import('../project-info').LibdragonInfo} libdragonInfo
32
- * @returns
33
32
  */
34
33
  function dockerRelativeWorkdirParams(libdragonInfo) {
35
34
  return ['--workdir', dockerRelativeWorkdir(libdragonInfo)];
36
35
  }
37
36
 
38
37
  /**
39
- *
40
38
  * @param {import('../project-info').LibdragonInfo} info
41
- * @returns
42
39
  */
43
40
  const exec = async (info) => {
44
41
  const parameters = info.options.EXTRA_PARAMS.slice(1);
@@ -49,20 +46,42 @@ const exec = async (info) => {
49
46
  true
50
47
  );
51
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
+
52
66
  const stdin = new PassThrough();
53
67
 
54
68
  /** @type {string[]} */
55
- const paramsWithConvertedPaths = parameters.map((item) => {
56
- if (item.startsWith('-')) {
69
+ const paramsWithConvertedPaths = await Promise.all(
70
+ parameters.map(async (item) => {
71
+ if (item.startsWith('-')) {
72
+ return item;
73
+ }
74
+ if (
75
+ item.includes(path.sep) &&
76
+ ((await fileExists(item)) || (await dirExists(item)))
77
+ ) {
78
+ return toPosixPath(
79
+ path.isAbsolute(item) ? path.relative(process.cwd(), item) : item
80
+ );
81
+ }
57
82
  return item;
58
- }
59
- if (item.includes(path.sep) && (fileExists(item) || dirExists(item))) {
60
- return toPosixPath(
61
- path.isAbsolute(item) ? path.relative(process.cwd(), item) : item
62
- );
63
- }
64
- return item;
65
- });
83
+ })
84
+ );
66
85
 
67
86
  /**
68
87
  *
@@ -101,9 +120,7 @@ const exec = async (info) => {
101
120
 
102
121
  let started = false;
103
122
  /**
104
- *
105
123
  * @param {import('fs').ReadStream=} stdin
106
- * @returns
107
124
  */
108
125
  const startOnceAndCmd = async (stdin) => {
109
126
  if (!started) {
@@ -147,14 +164,16 @@ const exec = async (info) => {
147
164
  // In the first run, pass the stdin to the process if it is not a TTY
148
165
  // o/w we loose a user input unnecesarily somehow.
149
166
  stdin:
150
- !process.stdin.isTTY &&
167
+ (!process.stdin.isTTY || undefined) &&
151
168
  /** @type {import('fs').ReadStream} */ (
152
169
  /** @type {unknown} */ (process.stdin)
153
170
  ),
154
171
  });
155
172
  } catch (e) {
173
+ if (!(e instanceof CommandError)) {
174
+ throw e;
175
+ }
156
176
  if (
157
- !e.out ||
158
177
  // TODO: is there a better way?
159
178
  !e.out.toString().includes(info.containerId)
160
179
  ) {
@@ -30,7 +30,7 @@ const { syncImageAndStart } = require('./update-and-start');
30
30
 
31
31
  /**
32
32
  * @param {import('../project-info').LibdragonInfo} info
33
- * @return {Promise<"submodule" | "subtree" | undefined>}
33
+ * @returns {Promise<"submodule" | "subtree" | undefined>}
34
34
  */
35
35
  const autoDetect = async (info) => {
36
36
  const vendorTarget = path.relative(
@@ -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);
@@ -188,6 +192,8 @@ const autoVendor = async (info) => {
188
192
  ]);
189
193
  return info;
190
194
  }
195
+
196
+ return info;
191
197
  };
192
198
 
193
199
  /**
@@ -218,31 +224,33 @@ async function init(info) {
218
224
  LIBDRAGON_PROJECT_MANIFEST
219
225
  )} exists. This is already a libdragon project, starting it...`
220
226
  );
221
- if (info.options.DOCKER_IMAGE) {
222
- log(
223
- `Not changing docker image. Use the install action if you want to override the image.`
224
- );
225
- }
226
- if (info.options.DOCKER_IMAGE) {
227
- info = await syncImageAndStart(info);
228
- } else {
229
- info = {
230
- ...info,
231
- containerId: await start(info),
232
- };
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
+ }
233
236
  }
237
+
234
238
  info = await autoVendor(info);
235
239
  await installDependencies(info);
236
240
  return info;
237
241
  }
238
242
 
239
- await updateImage(info, info.imageName);
240
-
241
- // Download image and start it
242
- info.containerId = await start(info);
243
-
244
- // We have created a new container, save the new info ASAP
245
- await initGitAndCacheContainerId(info);
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
+ }
246
254
 
247
255
  info = await autoVendor(info);
248
256
 
@@ -270,7 +278,7 @@ module.exports = /** @type {const} */ ({
270
278
 
271
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.
272
280
 
273
- 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.
274
282
 
275
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\`.`,
276
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
@@ -55,59 +55,61 @@ const installNPMDependencies = async (libdragonInfo) => {
55
55
 
56
56
  await Promise.all(
57
57
  deps.map(({ name, paths }) => {
58
- return new Promise((resolve, reject) => {
59
- fsClassic.access(
60
- path.join(paths[0], 'Makefile'),
61
- fsClassic.constants.F_OK,
62
- async (e) => {
63
- if (e) {
64
- // File does not exist - skip
65
- resolve();
66
- return;
67
- }
58
+ return /** @type Promise<void> */ (
59
+ new Promise((resolve, reject) => {
60
+ fsClassic.access(
61
+ path.join(paths[0], 'Makefile'),
62
+ fsClassic.constants.F_OK,
63
+ async (e) => {
64
+ if (e) {
65
+ // File does not exist - skip
66
+ resolve();
67
+ return;
68
+ }
68
69
 
69
- if (paths.length > 1) {
70
- reject(
71
- new ValidationError(
72
- `Using same dependency with different versions is not supported! ${name}`
73
- )
74
- );
75
- return;
76
- }
70
+ if (paths.length > 1) {
71
+ reject(
72
+ new ValidationError(
73
+ `Using same dependency with different versions is not supported! ${name}`
74
+ )
75
+ );
76
+ return;
77
+ }
77
78
 
78
- try {
79
- const relativePath = toPosixPath(
80
- path.relative(libdragonInfo.root, paths[0])
81
- );
82
- const containerPath = path.posix.join(
83
- CONTAINER_TARGET_PATH,
84
- relativePath
85
- );
86
- const makePath = path.posix.join(containerPath, 'Makefile');
79
+ try {
80
+ const relativePath = toPosixPath(
81
+ path.relative(libdragonInfo.root, paths[0])
82
+ );
83
+ const containerPath = path.posix.join(
84
+ CONTAINER_TARGET_PATH,
85
+ relativePath
86
+ );
87
+ const makePath = path.posix.join(containerPath, 'Makefile');
87
88
 
88
- await dockerExec(
89
- libdragonInfo,
90
- [...dockerHostUserParams(libdragonInfo)],
91
- [
92
- '/bin/bash',
93
- '-c',
94
- '[ -f ' +
95
- makePath +
96
- ' ] && make -C ' +
97
- containerPath +
98
- ' && make -C ' +
99
- containerPath +
100
- ' install',
101
- ]
102
- );
89
+ await dockerExec(
90
+ libdragonInfo,
91
+ [...dockerHostUserParams(libdragonInfo)],
92
+ [
93
+ '/bin/bash',
94
+ '-c',
95
+ '[ -f ' +
96
+ makePath +
97
+ ' ] && make -C ' +
98
+ containerPath +
99
+ ' && make -C ' +
100
+ containerPath +
101
+ ' install',
102
+ ]
103
+ );
103
104
 
104
- resolve();
105
- } catch (e) {
106
- reject(e);
105
+ resolve();
106
+ } catch (e) {
107
+ reject(e);
108
+ }
107
109
  }
108
- }
109
- );
110
- });
110
+ );
111
+ })
112
+ );
111
113
  })
112
114
  );
113
115
  }
@@ -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,11 +1,18 @@
1
- const { spawnProcess } = require('../helpers');
1
+ const { spawnProcess, ValidationError } = require('../helpers');
2
2
 
3
3
  const { checkContainerRunning } = require('./utils');
4
4
 
5
5
  /**
6
6
  * @param {import('../project-info').LibdragonInfo} libdragonInfo
7
+ * @returns {Promise<import('../project-info').LibdragonInfo | void>}
7
8
  */
8
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
+
9
16
  const running =
10
17
  libdragonInfo.containerId &&
11
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',
@@ -121,7 +131,6 @@ const destroyContainer = async (libdragonInfo) => {
121
131
  * @param {import('../project-info').LibdragonInfo} libdragonInfo
122
132
  * @param {string[]} params
123
133
  * @param {import('../helpers').SpawnOptions} options
124
- * @returns
125
134
  */
126
135
  async function runGitMaybeHost(libdragonInfo, params, options = {}) {
127
136
  assert(
@@ -145,6 +154,11 @@ async function runGitMaybeHost(libdragonInfo, params, options = {}) {
145
154
  throw e;
146
155
  }
147
156
 
157
+ assert(
158
+ !process.env.DOCKER_CONTAINER,
159
+ new Error('[runGitMaybeHost] Native git should exist in a container.')
160
+ );
161
+
148
162
  return await dockerExec(
149
163
  libdragonInfo,
150
164
  // Use the host user when initializing git as we will need access
@@ -160,6 +174,13 @@ async function runGitMaybeHost(libdragonInfo, params, options = {}) {
160
174
  * @param {import('../project-info').LibdragonInfo} libdragonInfo
161
175
  */
162
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
+
163
184
  const id =
164
185
  libdragonInfo.containerId &&
165
186
  (
@@ -189,6 +210,13 @@ async function checkContainerAndClean(libdragonInfo) {
189
210
  * @param {string} containerId
190
211
  */
191
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
+
192
220
  const running = (
193
221
  await spawnProcess('docker', [
194
222
  'container',
@@ -201,14 +229,19 @@ async function checkContainerRunning(containerId) {
201
229
  }
202
230
 
203
231
  /**
204
- * @param {import('../project-info').LibdragonInfo} libdragonInfo
232
+ * @param {import('../project-info').LibdragonInfo & {containerId: string}} libdragonInfo
205
233
  */
206
234
  async function initGitAndCacheContainerId(libdragonInfo) {
235
+ if (!libdragonInfo.containerId) {
236
+ return;
237
+ }
238
+
207
239
  // If there is managed vendoring, make sure we have a git repo. `git init` is
208
240
  // safe anyways...
209
241
  if (libdragonInfo.vendorStrategy !== 'manual') {
210
242
  await runGitMaybeHost(libdragonInfo, ['init']);
211
243
  }
244
+
212
245
  const gitFolder = path.join(libdragonInfo.root, '.git');
213
246
  if (await dirExists(gitFolder)) {
214
247
  await fs.writeFile(
@@ -11,6 +11,11 @@ module.exports = /** @type {const} */ ({
11
11
 
12
12
  ACCEPTED_STRATEGIES: ['submodule', 'subtree', 'manual'],
13
13
 
14
+ // These do not need a project to exist and their actions do not need the whole
15
+ // structure. Actions that need the full project information should not be
16
+ // listed here.
17
+ NO_PROJECT_ACTIONS: ['help', 'version'],
18
+
14
19
  // cli exit codes
15
20
  STATUS_OK: 0,
16
21
  STATUS_ERROR: 1,
@@ -5,6 +5,7 @@ const chalk = require('chalk').stderr;
5
5
  const { spawn } = require('child_process');
6
6
 
7
7
  const { globals } = require('./globals');
8
+ const { NO_PROJECT_ACTIONS } = require('./constants');
8
9
 
9
10
  /**
10
11
  * A structure to keep additional error information
@@ -156,6 +157,10 @@ function spawnProcess(
156
157
  spawnOptions: {},
157
158
  }
158
159
  ) {
160
+ assert(
161
+ cmd !== 'docker' || !process.env.DOCKER_CONTAINER,
162
+ new Error('Trying to invoke docker inside a container.')
163
+ );
159
164
  return new Promise((resolve, reject) => {
160
165
  /** @type {Buffer[]} */
161
166
  const stdout = [];
@@ -197,26 +202,26 @@ function spawnProcess(
197
202
  resolve('');
198
203
  };
199
204
 
200
- if (!enableInTTY && stdin) {
205
+ if (!enableInTTY && stdin && command.stdin) {
201
206
  stdin.pipe(command.stdin);
202
207
  }
203
208
 
204
- if (!enableOutTTY && (globals.verbose || userCommand)) {
209
+ if (!enableOutTTY && (globals.verbose || userCommand) && command.stdout) {
205
210
  command.stdout.pipe(process.stdout);
206
211
  process.stdout.once('error', eatEpipe);
207
212
  }
208
213
 
209
- if (!inheritStdout) {
214
+ if (!inheritStdout && command.stdout) {
210
215
  command.stdout.on('data', function (data) {
211
216
  stdout.push(Buffer.from(data));
212
217
  });
213
218
  }
214
219
 
215
- if (!enableErrorTTY && (globals.verbose || userCommand)) {
220
+ if (!enableErrorTTY && (globals.verbose || userCommand) && command.stderr) {
216
221
  command.stderr.pipe(process.stderr);
217
222
  }
218
223
 
219
- if (!inheritStderr) {
224
+ if (!inheritStderr && command.stderr) {
220
225
  command.stderr.on('data', function (data) {
221
226
  stderr.push(Buffer.from(data));
222
227
  });
@@ -264,58 +269,83 @@ function spawnProcess(
264
269
  * (libdragonInfo: import('./project-info').LibdragonInfo, cmdWithParams: string[], options?: SpawnOptions, unused?: unknown): Promise<string>;
265
270
  * }} DockerExec
266
271
  */
267
- const dockerExec = /** @type {DockerExec} */ function (
268
- libdragonInfo,
269
- dockerParams,
270
- cmdWithParams,
271
- /** @type {SpawnOptions} */
272
- options
273
- ) {
274
- assert(
275
- !!libdragonInfo.containerId,
276
- new Error('Trying to invoke dockerExec without a containerId.')
277
- );
272
+ const dockerExec = /** @type {DockerExec} */ (
273
+ function (
274
+ libdragonInfo,
275
+ dockerParams,
276
+ cmdWithParams,
277
+ /** @type {SpawnOptions | undefined} */
278
+ options
279
+ ) {
280
+ // TODO: assert for invalid args
281
+ const haveDockerParams =
282
+ Array.isArray(dockerParams) && Array.isArray(cmdWithParams);
278
283
 
279
- // TODO: assert for invalid args
280
- const haveDockerParams =
281
- Array.isArray(dockerParams) && Array.isArray(cmdWithParams);
284
+ if (!haveDockerParams) {
285
+ options = /** @type {SpawnOptions} */ (cmdWithParams);
286
+ }
282
287
 
283
- if (!haveDockerParams) {
284
- options = /** @type {SpawnOptions} */ (cmdWithParams);
285
- }
288
+ const finalCmdWithParams = haveDockerParams ? cmdWithParams : dockerParams;
289
+ const finalDockerParams = haveDockerParams ? dockerParams : [];
286
290
 
287
- const additionalParams = [];
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
+ );
288
297
 
289
- // Docker TTY wants in & out streams both to be a TTY
290
- // If no options are provided, disable TTY as spawnProcess defaults to no
291
- // inherit as well.
292
- const enableTTY = options
293
- ? options.inheritStdout && options.inheritStdin
294
- : false;
295
- const ttyEnabled = enableTTY && process.stdout.isTTY && process.stdin.isTTY;
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
+ }
296
310
 
297
- if (ttyEnabled) {
298
- additionalParams.push('-t');
299
- }
311
+ assert(
312
+ !!libdragonInfo.containerId,
313
+ new Error('Trying to invoke dockerExec without a containerId.')
314
+ );
300
315
 
301
- // Always enable stdin, also see; https://github.com/anacierdem/libdragon-docker/issues/45
302
- // Currently we run all exec commands in stdin mode even if the actual process
303
- // does not need any input. This will eat any user input by default.
304
- additionalParams.push('-i');
305
-
306
- return spawnProcess(
307
- 'docker',
308
- [
309
- 'exec',
310
- ...(haveDockerParams
311
- ? [...dockerParams, ...additionalParams]
312
- : additionalParams),
313
- libdragonInfo.containerId,
314
- ...(haveDockerParams ? cmdWithParams : dockerParams),
315
- ],
316
- options
317
- );
318
- };
316
+ /** @type string[] */
317
+ const additionalParams = [];
318
+
319
+ // Docker TTY wants in & out streams both to be a TTY
320
+ // If no options are provided, disable TTY as spawnProcess defaults to no
321
+ // inherit as well.
322
+ const enableTTY = options
323
+ ? options.inheritStdout && options.inheritStdin
324
+ : false;
325
+ const ttyEnabled = enableTTY && process.stdout.isTTY && process.stdin.isTTY;
326
+
327
+ if (ttyEnabled) {
328
+ additionalParams.push('-t');
329
+ }
330
+
331
+ // Always enable stdin, also see; https://github.com/anacierdem/libdragon-docker/issues/45
332
+ // Currently we run all exec commands in stdin mode even if the actual process
333
+ // does not need any input. This will eat any user input by default.
334
+ additionalParams.push('-i');
335
+
336
+ return spawnProcess(
337
+ 'docker',
338
+ [
339
+ 'exec',
340
+ ...finalDockerParams,
341
+ ...additionalParams,
342
+ libdragonInfo.containerId,
343
+ ...finalCmdWithParams,
344
+ ],
345
+ options
346
+ );
347
+ }
348
+ );
319
349
 
320
350
  /**
321
351
  * Recursively copies directories and files
@@ -386,8 +416,9 @@ function toNativePath(p) {
386
416
  }
387
417
 
388
418
  /**
389
- * @param {boolean} condition
419
+ * @param {any} condition
390
420
  * @param {Error} error
421
+ * @returns {asserts condition}
391
422
  */
392
423
  function assert(condition, error) {
393
424
  if (!condition) {
@@ -422,6 +453,18 @@ function log(text, verboseOnly = false) {
422
453
  }
423
454
  }
424
455
 
456
+ /**
457
+ * @param {import('./project-info').CLIInfo | import('./project-info').LibdragonInfo} info
458
+ * @returns {info is import('./project-info').LibdragonInfo}
459
+ */
460
+ function isProjectAction(info) {
461
+ return !NO_PROJECT_ACTIONS.includes(
462
+ /** @type {import('./project-info').ActionsNoProject} */ (
463
+ info.options.CURRENT_ACTION.name
464
+ )
465
+ );
466
+ }
467
+
425
468
  module.exports = {
426
469
  spawnProcess,
427
470
  toPosixPath,
@@ -436,4 +479,5 @@ module.exports = {
436
479
  CommandError,
437
480
  ParameterError,
438
481
  ValidationError,
482
+ isProjectAction,
439
483
  };
@@ -26,7 +26,7 @@ const { globals } = require('./globals');
26
26
  * @param {string[]} argv
27
27
  */
28
28
  const parseParameters = async (argv) => {
29
- /** @type CommandlineOptions */
29
+ /** @type Partial<CommandlineOptions> & {EXTRA_PARAMS: string[] } */
30
30
  const options = {
31
31
  EXTRA_PARAMS: [],
32
32
  CURRENT_ACTION: undefined,
@@ -143,7 +143,7 @@ const parseParameters = async (argv) => {
143
143
  process.exit(STATUS_BAD_PARAM);
144
144
  }
145
145
 
146
- return { options };
146
+ return { options: /** @type CommandlineOptions */ (options) };
147
147
  };
148
148
 
149
149
  module.exports = {
@@ -27,39 +27,48 @@ const {
27
27
  toPosixPath,
28
28
  assert,
29
29
  ParameterError,
30
+ isProjectAction,
30
31
  } = require('./helpers');
31
32
 
32
- // These do not need a project to exist.
33
- const NO_PROJECT_ACTIONS = /** @type {const} */ (['help', 'version']);
34
-
35
33
  /**
36
- * @typedef { typeof NO_PROJECT_ACTIONS[number] } ActionsNoProject
34
+ * @typedef { typeof import('./constants').NO_PROJECT_ACTIONS[number] } ActionsNoProject
37
35
  * @typedef { Exclude<keyof import('./parameters').Actions, ActionsNoProject> } ActionsWithProject
38
36
  * @typedef { import('./parameters').CommandlineOptions<ActionsNoProject> } ActionsNoProjectOptions
39
37
  * @typedef { import('./parameters').CommandlineOptions<ActionsWithProject> } ActionsWithProjectOptions
40
38
  *
39
+ * This is all the potential CLI combinations
41
40
  * @typedef { {
42
- * options: ActionsNoProjectOptions
41
+ * options: import('./parameters').CommandlineOptions
43
42
  * } } CLIInfo
44
43
  *
44
+ * Then readProjectInfo creates two possible set of outputs. One is for actions
45
+ * that don't need a project and one with project. This setup forces the actions
46
+ * to not use detailed information if they are listed in NO_PROJECT_ACTIONS
47
+ * @typedef { {
48
+ * options: ActionsNoProjectOptions
49
+ * } } NoProjectInfo
50
+ *
45
51
  * @typedef { {
52
+ * options: ActionsWithProjectOptions
46
53
  * root: string;
47
54
  * userInfo: os.UserInfo<string>;
48
55
  * haveProjectConfig: boolean;
49
56
  * imageName: string;
50
57
  * vendorDirectory: string;
51
58
  * vendorStrategy: import('./parameters').VendorStrategy;
52
- * containerId: string
53
- * } } ExtendedInfo
54
- *
55
- * @typedef { CLIInfo & Partial<ExtendedInfo> } LibdragonInfo
59
+ * containerId?: string
60
+ * } } LibdragonInfo
56
61
  */
57
62
 
58
63
  /**
59
64
  * @param {LibdragonInfo} libdragonInfo
60
- * @returns string
61
65
  */
62
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
+
63
72
  const idFile = path.join(libdragonInfo.root, '.git', CACHED_CONTAINER_FILE);
64
73
  if (await fileExists(idFile)) {
65
74
  const id = (await fs.readFile(idFile, { encoding: 'utf8' })).trim();
@@ -109,7 +118,7 @@ async function findContainerId(libdragonInfo) {
109
118
  /**
110
119
  * @param {string} start
111
120
  * @param {string} relativeFile
112
- * @return {Promise<string>}
121
+ * @returns {Promise<string | undefined>}
113
122
  */
114
123
  async function findLibdragonRoot(
115
124
  start = '.',
@@ -139,18 +148,15 @@ async function findGitRoot() {
139
148
  }
140
149
 
141
150
  /**
142
- * @param {LibdragonInfo} optionInfo
151
+ * @param {CLIInfo} optionInfo
152
+ * @returns {Promise<LibdragonInfo | NoProjectInfo>}
143
153
  */
144
154
  const readProjectInfo = async function (optionInfo) {
145
155
  // No need to do anything here if the action does not depend on the project
146
156
  // The only exception is the init and destroy actions, which do not need an
147
157
  // existing project but readProjectInfo must always run to analyze the situation
148
- if (
149
- NO_PROJECT_ACTIONS.includes(
150
- /** @type {ActionsNoProject} */ (optionInfo.options.CURRENT_ACTION.name)
151
- )
152
- ) {
153
- return /** @type {CLIInfo} */ (optionInfo);
158
+ if (!isProjectAction(optionInfo)) {
159
+ return /** @type {NoProjectInfo} */ (optionInfo);
154
160
  }
155
161
 
156
162
  const migratedRoot = await findLibdragonRoot();
@@ -173,10 +179,16 @@ const readProjectInfo = async function (optionInfo) {
173
179
  );
174
180
  }
175
181
 
182
+ const foundRoot =
183
+ projectRoot ?? (await findNPMRoot()) ?? (await findGitRoot());
184
+ if (!foundRoot) {
185
+ log('Could not find project root, set as cwd.', true);
186
+ }
187
+
176
188
  /** @type {LibdragonInfo} */
177
189
  let info = {
178
190
  ...optionInfo,
179
- root: projectRoot ?? (await findNPMRoot()) ?? (await findGitRoot()),
191
+ root: foundRoot ?? process.cwd(),
180
192
  userInfo: os.userInfo(),
181
193
 
182
194
  // Use this to discriminate if there is a project when the command is run
@@ -190,11 +202,6 @@ const readProjectInfo = async function (optionInfo) {
190
202
  vendorStrategy: DEFAULT_STRATEGY,
191
203
  };
192
204
 
193
- if (!info.root) {
194
- log('Could not find project root, set as cwd.', true);
195
- info.root = process.cwd();
196
- }
197
-
198
205
  log(`Project root: ${info.root}`, true);
199
206
 
200
207
  if (migratedRoot) {
@@ -223,15 +230,23 @@ const readProjectInfo = async function (optionInfo) {
223
230
  }
224
231
  }
225
232
 
226
- info.containerId = await findContainerId(info);
227
- 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
+ }
228
237
 
229
238
  // For imageName, flag has the highest priority followed by the one read from
230
239
  // the file and then if there is any matching container, name is read from it.
231
240
  // As last option fallback to default value.
232
241
 
233
242
  // If still have the container, read the image name from it
234
- if (!info.imageName && (await checkContainerAndClean(info))) {
243
+ // No need to do anything if we are in a container
244
+ if (
245
+ !process.env.DOCKER_CONTAINER &&
246
+ !info.imageName &&
247
+ info.containerId &&
248
+ (await checkContainerAndClean(info))
249
+ ) {
235
250
  info.imageName = (
236
251
  await spawnProcess('docker', [
237
252
  'container',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libdragon",
3
- "version": "10.8.1",
3
+ "version": "10.9.0",
4
4
  "description": "This is a docker wrapper for libdragon",
5
5
  "main": "index.js",
6
6
  "engines": {