libdragon 10.6.0 → 10.7.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.
@@ -1,7 +1,7 @@
1
1
  const path = require('path');
2
2
  const fs = require('fs/promises');
3
3
  const fsClassic = require('fs');
4
- const chalk = require('chalk');
4
+ const chalk = require('chalk').stderr;
5
5
  const { spawn } = require('child_process');
6
6
 
7
7
  const { globals } = require('./globals');
@@ -18,8 +18,9 @@ class CommandError extends Error {
18
18
 
19
19
  // The user provided an unexpected input
20
20
  class ParameterError extends Error {
21
- constructor(message) {
21
+ constructor(message, actionName) {
22
22
  super(message);
23
+ this.actionName = actionName;
23
24
  }
24
25
  }
25
26
 
@@ -50,51 +51,90 @@ async function dirExists(path) {
50
51
  });
51
52
  }
52
53
 
53
- // A simple Promise wrapper for child_process.spawn. If interactive is true,
54
- // stdout becomes a tty when available and we cannot read the stdout from the
55
- // command anymore. If interactive is "full", the error stream is also piped to
56
- // the main process as TTY, so it is not readable anymore as well. For the
57
- // commands using stderr as TTY it should be set to "full"
58
- function spawnProcess(cmd, params = [], userCommand, interactive = false) {
54
+ // A simple Promise wrapper for child_process.spawn. Return the err/out streams
55
+ // from the process by default. Specify inheritStdout / inheritStderr to disable
56
+ // this and inherit the parent process's stream, passing through the TTY if any.
57
+ function spawnProcess(
58
+ cmd,
59
+ params = [],
60
+ {
61
+ // Used to decorate the potential CommandError with a prop such that we can
62
+ // report this error back to the user.
63
+ userCommand = false,
64
+ // This is the stream where the process will receive its input.
65
+ stdin,
66
+ // If this is true, the related stream is inherited from the parent process
67
+ // and we cannot read them anymore. So if you need to read a stream, you
68
+ // should disable it. When disabled, the relevant stream cannot be a tty
69
+ // anymore. By default, we expect the caller to read err/out.
70
+ inheritStdin = true,
71
+ inheritStdout = false,
72
+ inheritStderr = false,
73
+ // Passthrough to spawn
74
+ spawnOptions = {},
75
+ } = {
76
+ userCommand: false,
77
+ inheritStdin: true,
78
+ inheritStdout: false,
79
+ inheritStderr: false,
80
+ spawnOptions: {},
81
+ }
82
+ ) {
59
83
  return new Promise((resolve, reject) => {
60
- let stdout = [];
61
- let stderr = [];
84
+ const stdout = [];
85
+ const stderr = [];
62
86
 
63
- if (globals.verbose) {
64
- log(chalk.grey(`Spawning: ${cmd} ${params.join(' ')}`), true);
65
- }
87
+ log(chalk.grey(`Spawning: ${cmd} ${params.join(' ')}`), true);
66
88
 
67
- const isTTY =
68
- process.stdin.isTTY && process.stdout.isTTY && process.stderr.isTTY;
69
- const enableTTY = isTTY && !!interactive;
70
- const enableErrorTTY = isTTY && interactive === 'full';
89
+ const enableInTTY = Boolean(process.stdin.isTTY) && inheritStdin;
90
+ const enableOutTTY = Boolean(process.stdout.isTTY) && inheritStdout;
91
+ const enableErrorTTY = Boolean(process.stderr.isTTY) && inheritStderr;
71
92
 
72
93
  const command = spawn(cmd, params, {
73
- // We should redirect streams together for the TTY to work
74
- // properly if they are all used as TTY
75
94
  stdio: [
76
- enableTTY ? 'inherit' : 'pipe',
77
- enableTTY ? 'inherit' : 'pipe',
95
+ enableInTTY ? 'inherit' : 'pipe',
96
+ enableOutTTY ? 'inherit' : 'pipe',
78
97
  enableErrorTTY ? 'inherit' : 'pipe',
79
98
  ],
99
+ ...spawnOptions,
80
100
  });
81
101
 
82
- if (!enableTTY && (globals.verbose || userCommand)) {
83
- command.stdout.pipe(process.stdout);
102
+ // See a very old related issue: https://github.com/nodejs/node/issues/947
103
+ // When the stream is not fully consumed by the pipe target and it exits,
104
+ // an EPIPE or EOF is thrown. We don't care about those.
105
+ const eatEpipe = (err) => {
106
+ if (err.code !== 'EPIPE' && err.code !== 'EOF') {
107
+ throw err;
108
+ }
109
+ // No need to listen for close anymore
110
+ command.off('close', closeHandler);
111
+
112
+ // It was not fully consumed, just resolve into an empty string
113
+ // No one should be using this anyways. Ideally we could clean a few
114
+ // last bytes from the buffers to create a correct utf-8 string.
115
+ resolve('');
116
+ };
117
+
118
+ if (!enableInTTY && stdin) {
119
+ stdin.pipe(command.stdin);
84
120
  }
85
121
 
86
- if (!enableErrorTTY && (globals.verbose || userCommand)) {
87
- command.stderr.pipe(process.stderr);
122
+ if (!enableOutTTY && (globals.verbose || userCommand)) {
123
+ command.stdout.pipe(process.stdout);
124
+ process.stdout.once('error', eatEpipe);
88
125
  }
89
126
 
90
- // We shouldn't need to collect the data if it is a user command.
91
- if (!enableTTY && !userCommand) {
127
+ if (!inheritStdout) {
92
128
  command.stdout.on('data', function (data) {
93
129
  stdout.push(Buffer.from(data));
94
130
  });
95
131
  }
96
132
 
97
- if (!enableErrorTTY) {
133
+ if (!enableErrorTTY && (globals.verbose || userCommand)) {
134
+ command.stderr.pipe(process.stderr);
135
+ }
136
+
137
+ if (!inheritStderr) {
98
138
  command.stderr.on('data', function (data) {
99
139
  stderr.push(Buffer.from(data));
100
140
  });
@@ -106,6 +146,9 @@ function spawnProcess(cmd, params = [], userCommand, interactive = false) {
106
146
  };
107
147
 
108
148
  const closeHandler = function (code) {
149
+ // The stream was fully consumed, if there is this an additional error on
150
+ // stdout, it must be a legitimate error
151
+ process.stdout.off('error', eatEpipe);
109
152
  command.off('error', errorHandler);
110
153
  if (code === 0) {
111
154
  resolve(Buffer.concat(stdout).toString());
@@ -127,20 +170,39 @@ function spawnProcess(cmd, params = [], userCommand, interactive = false) {
127
170
  });
128
171
  }
129
172
 
130
- function dockerExec(
131
- libdragonInfo,
132
- dockerParams,
133
- cmdWithParams,
134
- userCommand,
135
- interactive
136
- ) {
173
+ function dockerExec(libdragonInfo, dockerParams, cmdWithParams, options) {
174
+ assert(
175
+ libdragonInfo.containerId,
176
+ new Error('Trying to invoke dockerExec without a containerId.')
177
+ );
178
+
137
179
  // TODO: assert for invalid args
138
180
  const haveDockerParams =
139
181
  Array.isArray(dockerParams) && Array.isArray(cmdWithParams);
140
- const isTTY = process.stdin.isTTY && process.stdout.isTTY;
141
- // interactive and TTY?
142
- const additionalParams =
143
- isTTY && (haveDockerParams ? interactive : userCommand) ? ['-it'] : [];
182
+
183
+ if (!haveDockerParams) {
184
+ options = cmdWithParams;
185
+ }
186
+
187
+ const additionalParams = [];
188
+
189
+ // Docker TTY wants in & out streams both to be a TTY
190
+ // If no options are provided, disable TTY as spawnProcess defaults to no
191
+ // inherit as well.
192
+ const enableTTY = options
193
+ ? options.inheritStdout && options.inheritStdin
194
+ : false;
195
+ const ttyEnabled = enableTTY && process.stdout.isTTY && process.stdin.isTTY;
196
+
197
+ if (ttyEnabled) {
198
+ additionalParams.push('-t');
199
+ }
200
+
201
+ // Always enable stdin, also see; https://github.com/anacierdem/libdragon-docker/issues/45
202
+ // Currently we run all exec commands in stdin mode even if the actual process
203
+ // does not need any input. This will eat any user input by default.
204
+ additionalParams.push('-i');
205
+
144
206
  return spawnProcess(
145
207
  'docker',
146
208
  [
@@ -151,8 +213,7 @@ function dockerExec(
151
213
  libdragonInfo.containerId,
152
214
  ...(haveDockerParams ? cmdWithParams : dockerParams),
153
215
  ],
154
- haveDockerParams ? userCommand : cmdWithParams,
155
- haveDockerParams ? interactive : userCommand
216
+ options
156
217
  );
157
218
  }
158
219
 
@@ -223,16 +284,21 @@ function assert(condition, error) {
223
284
  }
224
285
  }
225
286
 
226
- // TODO: we can handle showStatus here
287
+ function print(text) {
288
+ // eslint-disable-next-line no-console
289
+ console.log(text);
290
+ return;
291
+ }
292
+
227
293
  function log(text, verboseOnly = false) {
228
294
  if (!verboseOnly) {
229
295
  // eslint-disable-next-line no-console
230
- console.log(text);
296
+ console.error(text);
231
297
  return;
232
298
  }
233
299
  if (globals.verbose) {
234
300
  // eslint-disable-next-line no-console
235
- console.log(chalk.gray(text));
301
+ console.error(chalk.gray(text));
236
302
  return;
237
303
  }
238
304
  }
@@ -241,6 +307,7 @@ module.exports = {
241
307
  spawnProcess,
242
308
  toPosixPath,
243
309
  toNativePath,
310
+ print,
244
311
  log,
245
312
  dockerExec,
246
313
  assert,
@@ -0,0 +1,129 @@
1
+ const chalk = require('chalk').stderr;
2
+
3
+ const { log } = require('./helpers');
4
+ const actions = require('./actions');
5
+ const { fn: printUsage } = require('./actions/help');
6
+ const { STATUS_BAD_PARAM } = require('./constants');
7
+ const { globals } = require('./globals');
8
+
9
+ const parseParameters = async (argv) => {
10
+ const options = {
11
+ EXTRA_PARAMS: [],
12
+ CURRENT_ACTION: undefined,
13
+ };
14
+
15
+ for (let i = 2; i < argv.length; i++) {
16
+ const val = argv[i];
17
+
18
+ if (['--verbose', '-v'].includes(val)) {
19
+ options.VERBOSE = true;
20
+ globals.verbose = true;
21
+ continue;
22
+ }
23
+
24
+ // TODO: we might move these to actions as well.
25
+ if (['--image', '-i'].includes(val)) {
26
+ options.DOCKER_IMAGE = argv[++i];
27
+ continue;
28
+ } else if (val.indexOf('--image=') === 0) {
29
+ options.DOCKER_IMAGE = val.split('=')[1];
30
+ continue;
31
+ }
32
+
33
+ if (['--directory', '-d'].includes(val)) {
34
+ options.VENDOR_DIR = argv[++i];
35
+ continue;
36
+ } else if (val.indexOf('--directory=') === 0) {
37
+ options.VENDOR_DIR = val.split('=')[1];
38
+ continue;
39
+ }
40
+
41
+ if (['--strategy', '-s'].includes(val)) {
42
+ options.VENDOR_STRAT = argv[++i];
43
+ continue;
44
+ } else if (val.indexOf('--strategy=') === 0) {
45
+ options.VENDOR_STRAT = val.split('=')[1];
46
+ continue;
47
+ }
48
+
49
+ if (['--file', '-f'].includes(val)) {
50
+ options.FILE = argv[++i];
51
+ continue;
52
+ } else if (val.indexOf('--file=') === 0) {
53
+ options.FILE = val.split('=')[1];
54
+ continue;
55
+ }
56
+
57
+ if (val.indexOf('-') == 0) {
58
+ log(chalk.red(`Invalid flag \`${val}\``));
59
+ printUsage();
60
+ process.exit(STATUS_BAD_PARAM);
61
+ }
62
+
63
+ if (options.CURRENT_ACTION) {
64
+ log(chalk.red(`Expected only a single action, found: \`${val}\``));
65
+ printUsage();
66
+ process.exit(STATUS_BAD_PARAM);
67
+ }
68
+
69
+ options.CURRENT_ACTION = actions[val];
70
+
71
+ if (!options.CURRENT_ACTION) {
72
+ log(chalk.red(`Invalid action \`${val}\``));
73
+ printUsage();
74
+ process.exit(STATUS_BAD_PARAM);
75
+ }
76
+
77
+ if (options.CURRENT_ACTION.forwardsRestParams) {
78
+ options.EXTRA_PARAMS = argv.slice(i + 1);
79
+ break;
80
+ }
81
+ }
82
+
83
+ if (!options.CURRENT_ACTION) {
84
+ log(chalk.red('No action provided'));
85
+ printUsage();
86
+ process.exit(STATUS_BAD_PARAM);
87
+ }
88
+
89
+ if (
90
+ options.CURRENT_ACTION === actions.exec &&
91
+ options.EXTRA_PARAMS.length === 0
92
+ ) {
93
+ log(chalk.red('You should provide a command to exec'));
94
+ printUsage(undefined, [options.CURRENT_ACTION.name]);
95
+ process.exit(STATUS_BAD_PARAM);
96
+ }
97
+
98
+ if (
99
+ ![actions.init, actions.install, actions.update].includes(
100
+ options.CURRENT_ACTION
101
+ ) &&
102
+ options.DOCKER_IMAGE
103
+ ) {
104
+ log(chalk.red('Invalid flag: image'));
105
+ printUsage(undefined, [options.CURRENT_ACTION.name]);
106
+ process.exit(STATUS_BAD_PARAM);
107
+ }
108
+
109
+ if (
110
+ options.VENDOR_STRAT &&
111
+ !['submodule', 'subtree', 'manual'].includes(options.VENDOR_STRAT)
112
+ ) {
113
+ log(chalk.red(`Invalid strategy \`${options.VENDOR_STRAT}\``));
114
+ printUsage();
115
+ process.exit(STATUS_BAD_PARAM);
116
+ }
117
+
118
+ if (![actions.disasm].includes(options.CURRENT_ACTION) && options.FILE) {
119
+ log(chalk.red('Invalid flag: file'));
120
+ printUsage(undefined, [options.CURRENT_ACTION.name]);
121
+ process.exit(STATUS_BAD_PARAM);
122
+ }
123
+
124
+ return { options };
125
+ };
126
+
127
+ module.exports = {
128
+ parseParameters,
129
+ };
@@ -4,8 +4,7 @@ const fs = require('fs/promises');
4
4
 
5
5
  const {
6
6
  checkContainerAndClean,
7
- runGitMaybeHost,
8
- tryCacheContainerId,
7
+ initGitAndCacheContainerId,
9
8
  } = require('./actions/utils');
10
9
 
11
10
  const { findNPMRoot } = require('./actions/npm-utils');
@@ -27,8 +26,12 @@ const {
27
26
  spawnProcess,
28
27
  toPosixPath,
29
28
  assert,
29
+ ParameterError,
30
30
  } = require('./helpers');
31
31
 
32
+ const initAction = require('./actions/init');
33
+ const destroyAction = require('./actions/destroy');
34
+
32
35
  async function findContainerId(libdragonInfo) {
33
36
  const idFile = path.join(libdragonInfo.root, '.git', CACHED_CONTAINER_FILE);
34
37
  if (await fileExists(idFile)) {
@@ -49,7 +52,13 @@ async function findContainerId(libdragonInfo) {
49
52
  ])
50
53
  )
51
54
  .split('\n')
52
- .filter((s) => s.includes(`${libdragonInfo.root} `));
55
+ // docker seem to save paths with posix separators but make sure we look for
56
+ // both, just in case
57
+ .filter(
58
+ (s) =>
59
+ s.includes(`${toPosixPath(libdragonInfo.root)} `) ||
60
+ s.includes(`${libdragonInfo.root} `)
61
+ );
53
62
 
54
63
  if (candidates.length > 0) {
55
64
  const str = candidates[0];
@@ -57,11 +66,12 @@ async function findContainerId(libdragonInfo) {
57
66
  const idIndex = str.indexOf(shortId);
58
67
  const longId = str.slice(idIndex, idIndex + 64);
59
68
  if (longId.length === 64) {
60
- // If there is managed vendoring, make sure we have a git repo
61
- if (libdragonInfo.vendorStrategy !== 'manual') {
62
- await runGitMaybeHost(libdragonInfo, ['init']);
63
- }
64
- await tryCacheContainerId({ ...libdragonInfo, containerId: longId });
69
+ const newInfo = { ...libdragonInfo, containerId: longId };
70
+ // This shouldn't happen but if the user somehow deleted the .git folder
71
+ // (we don't have the container id file at this point) we can recover the
72
+ // project. `git init` is safe anyways and it is not executed if strategy
73
+ // is `manual`
74
+ await initGitAndCacheContainerId(newInfo);
65
75
  return longId;
66
76
  }
67
77
  }
@@ -90,14 +100,36 @@ async function findGitRoot() {
90
100
  }
91
101
  }
92
102
 
93
- async function readProjectInfo() {
103
+ async function readProjectInfo(info) {
104
+ // No need to do anything here if the action does not depend on the project
105
+ // The only exception is the init and destroy actions, which do not need an
106
+ // existing project but readProjectInfo must always run to analyze the situation
107
+ const forceReadProjectInfo = [initAction, destroyAction].includes(
108
+ info.options.CURRENT_ACTION
109
+ );
110
+ if (
111
+ info.options.CURRENT_ACTION.mustHaveProject === false &&
112
+ !forceReadProjectInfo
113
+ ) {
114
+ return info;
115
+ }
116
+
94
117
  const projectRoot = await findLibdragonRoot();
95
118
 
96
- let info = {
119
+ if (!projectRoot && !forceReadProjectInfo) {
120
+ throw new ParameterError(
121
+ 'This is not a libdragon project. Initialize with `libdragon init` first.',
122
+ info.options.CURRENT_ACTION.name
123
+ );
124
+ }
125
+
126
+ info = {
127
+ ...info,
97
128
  root: projectRoot ?? (await findNPMRoot()) ?? (await findGitRoot()),
98
129
  userInfo: os.userInfo(),
99
130
 
100
131
  // Use this to discriminate if there is a project when the command is run
132
+ // Only used for the init action ATM, and it is not ideal to have this here
101
133
  haveProjectConfig: !!projectRoot,
102
134
 
103
135
  // Set the defaults immediately, these should be present at all times even
@@ -169,7 +201,7 @@ async function readProjectInfo() {
169
201
  }
170
202
 
171
203
  /**
172
- * @param info This is only the base info without action properties like showStatus
204
+ * @param info This is only the base info without options
173
205
  * fn and command line options
174
206
  */
175
207
  async function writeProjectInfo(info) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libdragon",
3
- "version": "10.6.0",
3
+ "version": "10.7.0",
4
4
  "description": "This is a docker wrapper for libdragon",
5
5
  "main": "index.js",
6
6
  "engines": {
package/skeleton/Makefile CHANGED
@@ -13,7 +13,7 @@ hello.z64: N64_ROM_TITLE="Hello World"
13
13
  $(BUILD_DIR)/hello.elf: $(OBJS)
14
14
 
15
15
  clean:
16
- rm -f $(BUILD_DIR) *.z64
16
+ rm -f $(BUILD_DIR)/* *.z64
17
17
  .PHONY: clean
18
18
 
19
19
  -include $(wildcard $(BUILD_DIR)/*.d)