libdragon 10.5.0 → 10.7.1

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/CHANGELOG.md CHANGED
@@ -1,6 +1,48 @@
1
1
  # Change Log
2
2
 
3
- ## [10.5.0] - 2022-04-09
3
+ ## [10.7.1] - 2022-04-20
4
+
5
+ ### Fixed
6
+
7
+ - Migrating from and old version was incorrectly erroring out to do an additional
8
+ `init`, which is not necessarily required.
9
+
10
+ ### Changed
11
+
12
+ - Do not print usage information when a command fails.
13
+
14
+ ## [10.7.0] - 2022-04-17
15
+
16
+ ### Fixed
17
+
18
+ - Logs properly goes to stderr now. Previously they were written to stdout. This
19
+ means the id output of the `start` action is now written to stdout while we can
20
+ also display other information on the terminal. This allowed enabling the docker
21
+ logs for a more responsive experience. The output of `help` still goes to stdout.
22
+
23
+ ### Added
24
+
25
+ - Stdin consumption support. Now it is possible to pipe anything to `exec` and
26
+ it will pass it through to the target. In case of no running container, it will
27
+ keep a copy of the stdin stream until the docker process is ready. This enables
28
+ piping in data from the host if ever needed for some reason. This enables usages
29
+ like `cat file.txt | libdragon exec cat - | less`.
30
+ - Automatically convert host paths into posix format so that the user can use
31
+ the host's path autocompletion. It will also convert absolute host paths into
32
+ relative container paths automatically. Previously all paths were assumed to be
33
+ container paths relative to the location corresponding to the host cwd.
34
+ Closes #24.
35
+
36
+ ### Changed
37
+
38
+ - Refactored process spawns.
39
+ - Refactored main flow and separated parsing logic.
40
+ - Reorder actions & correction on flag usage for help output.
41
+ - Setting `--verbose` for `start` does not guarantee the only-id output anymore.
42
+ - Refactored parameter parsing.
43
+ - Update submdule for local development.
44
+
45
+ ## [10.6.0] - 2022-04-09
4
46
  ### Fixed
5
47
 
6
48
  - Fix a path bug that would cause incorrect behaviour when the command is run
@@ -14,7 +56,7 @@ were saving project info mistakenly.
14
56
 
15
57
  - `disasm` action to simplify disassembling ELF files generated by the toolchain.
16
58
  - `version` action to display current version.
17
- - `clean` action to remove the libdragon project.
59
+ - `destroy` action to remove the libdragon project.
18
60
  - Additional documentation for flags.
19
61
  - Print duration information when verbose.
20
62
 
package/index.js CHANGED
@@ -1,9 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const chalk = require('chalk');
3
+ const chalk = require('chalk').stderr;
4
4
 
5
- const actions = require('./modules/actions');
6
- const { fn: printUsage } = require('./modules/actions/help');
7
5
  const {
8
6
  STATUS_OK,
9
7
  STATUS_BAD_PARAM,
@@ -17,140 +15,19 @@ const {
17
15
  ValidationError,
18
16
  log,
19
17
  } = require('./modules/helpers');
18
+ const { parseParameters } = require('./modules/parameters');
20
19
  const { readProjectInfo, writeProjectInfo } = require('./modules/project-info');
21
20
 
22
- let options = {},
23
- currentAction;
24
-
25
- for (let i = 2; i < process.argv.length; i++) {
26
- const val = process.argv[i];
27
-
28
- // Allow console here
29
- /* eslint-disable no-console */
30
-
31
- if (['--verbose', '-v'].includes(val)) {
32
- options.VERBOSE = true;
33
- globals.verbose = true;
34
- continue;
35
- }
36
-
37
- // TODO: we might move these to actions as well.
38
- if (['--image', '-i'].includes(val)) {
39
- options.DOCKER_IMAGE = process.argv[++i];
40
- continue;
41
- } else if (val.indexOf('--image=') === 0) {
42
- options.DOCKER_IMAGE = val.split('=')[1];
43
- continue;
44
- }
45
-
46
- if (['--directory', '-d'].includes(val)) {
47
- options.VENDOR_DIR = process.argv[++i];
48
- continue;
49
- } else if (val.indexOf('--directory=') === 0) {
50
- options.VENDOR_DIR = val.split('=')[1];
51
- continue;
52
- }
53
-
54
- if (['--strategy', '-s'].includes(val)) {
55
- options.VENDOR_STRAT = process.argv[++i];
56
- continue;
57
- } else if (val.indexOf('--strategy=') === 0) {
58
- options.VENDOR_STRAT = val.split('=')[1];
59
- continue;
60
- }
61
-
62
- if (['--file', '-f'].includes(val)) {
63
- options.FILE = process.argv[++i];
64
- continue;
65
- } else if (val.indexOf('--file=') === 0) {
66
- options.FILE = val.split('=')[1];
67
- continue;
68
- }
69
-
70
- if (val.indexOf('--') >= 0) {
71
- console.error(chalk.red(`Invalid flag \`${val}\``));
72
- printUsage();
73
- process.exit(STATUS_BAD_PARAM);
74
- }
75
-
76
- if (currentAction) {
77
- console.error(
78
- chalk.red(`Expected only a single action, found: \`${val}\``)
79
- );
80
- printUsage();
81
- process.exit(STATUS_BAD_PARAM);
82
- }
83
-
84
- currentAction = actions[val];
85
-
86
- if (!currentAction) {
87
- console.error(chalk.red(`Invalid action \`${val}\``));
88
- printUsage();
89
- process.exit(STATUS_BAD_PARAM);
90
- }
91
-
92
- if (currentAction.forwardsRestParams) {
93
- options.PARAMS = process.argv.slice(i + 1);
94
- break;
95
- }
96
- }
97
-
98
- if (!currentAction) {
99
- console.error(chalk.red('No action provided'));
100
- printUsage();
101
- process.exit(STATUS_BAD_PARAM);
102
- }
103
-
104
- if (currentAction === actions.exec && options.PARAMS.length === 0) {
105
- console.error(chalk.red('You should provide a command to exec'));
106
- printUsage(undefined, [currentAction.name]);
107
- process.exit(STATUS_BAD_PARAM);
108
- }
109
-
110
- if (
111
- ![actions.init, actions.install, actions.update].includes(currentAction) &&
112
- options.DOCKER_IMAGE
113
- ) {
114
- console.error(chalk.red('Invalid flag: image'));
115
- printUsage(undefined, [currentAction.name]);
116
- process.exit(STATUS_BAD_PARAM);
117
- }
118
-
119
- if (
120
- options.VENDOR_STRAT &&
121
- !['submodule', 'subtree', 'manual'].includes(options.VENDOR_STRAT)
122
- ) {
123
- console.error(chalk.red(`Invalid strategy \`${options.VENDOR_STRAT}\``));
124
- printUsage();
125
- process.exit(STATUS_BAD_PARAM);
126
- }
127
-
128
- if (![actions.disasm].includes(currentAction) && options.FILE) {
129
- console.error(chalk.red('Invalid flag: file'));
130
- printUsage(undefined, [currentAction.name]);
131
- process.exit(STATUS_BAD_PARAM);
132
- }
133
-
134
- readProjectInfo()
135
- .then((info) =>
136
- currentAction.fn(
137
- {
138
- ...info,
139
- options,
140
- ...currentAction,
141
- },
142
- options.PARAMS ?? []
143
- )
144
- )
21
+ parseParameters(process.argv)
22
+ .then(readProjectInfo)
23
+ .then((info) => info.options.CURRENT_ACTION.fn(info))
145
24
  .catch((e) => {
146
25
  if (e instanceof ParameterError) {
147
- console.error(chalk.red(e.message));
148
- printUsage(undefined, [currentAction.name]);
26
+ log(chalk.red(e.message));
149
27
  process.exit(STATUS_BAD_PARAM);
150
28
  }
151
-
152
29
  if (e instanceof ValidationError) {
153
- console.error(chalk.red(e.message));
30
+ log(chalk.red(e.message));
154
31
  process.exit(STATUS_VALIDATION_ERROR);
155
32
  }
156
33
 
@@ -158,7 +35,7 @@ readProjectInfo()
158
35
 
159
36
  // Show additional information to user if verbose or we did a mistake
160
37
  if (globals.verbose || !userTargetedError) {
161
- console.error(chalk.red(globals.verbose ? e.stack : e.message));
38
+ log(chalk.red(globals.verbose ? e.stack : e.message));
162
39
  }
163
40
 
164
41
  // Print the underlying error out only if not verbose and we did a mistake
@@ -1,13 +1,12 @@
1
1
  const fs = require('fs/promises');
2
2
  const path = require('path');
3
3
 
4
- const { mustHaveProject, destroyContainer } = require('./utils');
4
+ const { destroyContainer } = require('./utils');
5
5
  const { CONFIG_FILE, LIBDRAGON_PROJECT_MANIFEST } = require('../constants');
6
- const { fileExists, dirExists } = require('../helpers');
7
-
8
- const clean = async (libdragonInfo) => {
9
- await mustHaveProject(libdragonInfo);
6
+ const { fileExists, dirExists, log } = require('../helpers');
7
+ const chalk = require('chalk');
10
8
 
9
+ const destroy = async (libdragonInfo) => {
11
10
  await destroyContainer(libdragonInfo);
12
11
 
13
12
  const projectPath = path.join(libdragonInfo.root, LIBDRAGON_PROJECT_MANIFEST);
@@ -16,20 +15,20 @@ const clean = async (libdragonInfo) => {
16
15
  if (await fileExists(configPath)) {
17
16
  await fs.rm(configPath);
18
17
  }
19
- if (dirExists(projectPath)) {
18
+ if (await dirExists(projectPath)) {
20
19
  await fs.rmdir(projectPath);
21
20
  }
21
+
22
+ log(chalk.green('Done cleanup.'));
22
23
  };
23
24
 
24
25
  module.exports = {
25
- name: 'clean',
26
- fn: clean,
27
- showStatus: true,
26
+ name: 'destroy',
27
+ fn: destroy,
28
+ mustHaveProject: false,
28
29
  usage: {
29
- name: 'clean',
30
+ name: 'destroy',
30
31
  summary: 'Do clean-up for current project.',
31
- description: `Removes libdragon configuration from current project and removes any known containers but will not touch previously vendored files. \`libdragon\` will not work anymore for this project.
32
-
33
- Must be run in an initialized libdragon project.`,
32
+ description: `Removes libdragon configuration from current project and removes any known containers but will not touch previously vendored files. \`libdragon\` will not work anymore for this project.`,
34
33
  },
35
34
  };
@@ -1,7 +1,6 @@
1
1
  const path = require('path');
2
2
  const fs = require('fs/promises');
3
3
 
4
- const { mustHaveProject } = require('./utils');
5
4
  const { fn: exec } = require('./exec');
6
5
  const {
7
6
  ValidationError,
@@ -44,49 +43,55 @@ const findElf = async (stop, start = '.') => {
44
43
  }
45
44
  };
46
45
 
47
- const disasm = async (libdragonInfo, extraArgs) => {
48
- await mustHaveProject(libdragonInfo);
49
-
46
+ const disasm = async (info) => {
50
47
  let elfPath;
51
- if (libdragonInfo.options.FILE) {
52
- if (
53
- path
54
- .relative(libdragonInfo.root, libdragonInfo.options.FILE)
55
- .startsWith('..')
56
- ) {
48
+ if (info.options.FILE) {
49
+ if (path.relative(info.root, info.options.FILE).startsWith('..')) {
57
50
  throw new ParameterError(
58
- `Provided file ${libdragonInfo.options.FILE} is outside the project directory.`
51
+ `Provided file ${info.options.FILE} is outside the project directory.`,
52
+ info.options.CURRENT_ACTION.name
59
53
  );
60
54
  }
61
- if (!(await fileExists(libdragonInfo.options.FILE)))
55
+ if (!(await fileExists(info.options.FILE)))
62
56
  throw new ParameterError(
63
- `Provided file ${libdragonInfo.options.FILE} does not exist`
57
+ `Provided file ${info.options.FILE} does not exist`,
58
+ info.options.CURRENT_ACTION.name
64
59
  );
65
- elfPath = libdragonInfo.options.FILE;
60
+ elfPath = info.options.FILE;
66
61
  }
67
- elfPath = elfPath ?? (await findElf(libdragonInfo.root));
62
+ elfPath = elfPath ?? (await findElf(info.root));
68
63
 
69
- const haveSymbol = extraArgs.length > 0 && !extraArgs[0].startsWith('-');
64
+ const haveSymbol =
65
+ info.options.EXTRA_PARAMS.length > 0 &&
66
+ !info.options.EXTRA_PARAMS[0].startsWith('-');
70
67
 
71
68
  const finalArgs = haveSymbol
72
- ? [`--disassemble=${extraArgs[0]}`, ...extraArgs.slice(1)]
73
- : extraArgs;
69
+ ? [
70
+ `--disassemble=${info.options.EXTRA_PARAMS[0]}`,
71
+ ...info.options.EXTRA_PARAMS.slice(1),
72
+ ]
73
+ : info.options.EXTRA_PARAMS;
74
74
 
75
75
  const intermixSourceParams =
76
- extraArgs.length === 0 || haveSymbol ? ['-S'] : [];
77
-
78
- return await exec(libdragonInfo, [
79
- 'mips64-elf-objdump',
80
- ...finalArgs,
81
- ...intermixSourceParams,
82
- toPosixPath(path.relative(process.cwd(), elfPath)),
83
- ]);
76
+ info.options.EXTRA_PARAMS.length === 0 || haveSymbol ? ['-S'] : [];
77
+
78
+ return await exec({
79
+ ...info,
80
+ options: {
81
+ ...info.options,
82
+ EXTRA_PARAMS: [
83
+ 'mips64-elf-objdump',
84
+ ...finalArgs,
85
+ ...intermixSourceParams,
86
+ toPosixPath(path.relative(process.cwd(), elfPath)),
87
+ ],
88
+ },
89
+ });
84
90
  };
85
91
 
86
92
  module.exports = {
87
93
  name: 'disasm',
88
94
  fn: disasm,
89
- showStatus: true,
90
95
  forwardsRestParams: true,
91
96
  usage: {
92
97
  name: 'disasm [symbol|flags]',
@@ -1,11 +1,18 @@
1
1
  const path = require('path');
2
+ const { PassThrough } = require('stream');
2
3
 
3
4
  const { CONTAINER_TARGET_PATH } = require('../constants');
4
- const { log, dockerExec, toPosixPath } = require('../helpers');
5
+ const {
6
+ log,
7
+ dockerExec,
8
+ toPosixPath,
9
+ fileExists,
10
+ dirExists,
11
+ } = require('../helpers');
5
12
 
6
13
  const { start } = require('./start');
7
14
  const { dockerHostUserParams } = require('./docker-utils');
8
- const { installDependencies, mustHaveProject } = require('./utils');
15
+ const { installDependencies } = require('./utils');
9
16
 
10
17
  function dockerRelativeWorkdir(libdragonInfo) {
11
18
  return (
@@ -19,72 +26,114 @@ function dockerRelativeWorkdirParams(libdragonInfo) {
19
26
  return ['--workdir', dockerRelativeWorkdir(libdragonInfo)];
20
27
  }
21
28
 
22
- const exec = async (libdragonInfo, commandAndParams) => {
23
- await mustHaveProject(libdragonInfo);
24
-
29
+ const exec = async (info) => {
30
+ const parameters = info.options.EXTRA_PARAMS.slice(1);
25
31
  log(
26
- `Running ${commandAndParams[0]} at ${dockerRelativeWorkdir(
27
- libdragonInfo
28
- )} with [${commandAndParams.slice(1)}]`,
32
+ `Running ${info.options.EXTRA_PARAMS[0]} at ${dockerRelativeWorkdir(
33
+ info
34
+ )} with [${parameters}]`,
29
35
  true
30
36
  );
31
37
 
32
- const tryCmd = (libdragonInfo) =>
33
- libdragonInfo.containerId &&
34
- dockerExec(
35
- libdragonInfo,
36
- [
37
- ...dockerRelativeWorkdirParams(libdragonInfo),
38
- ...dockerHostUserParams(libdragonInfo),
39
- ],
40
- commandAndParams,
41
- true,
42
- true // Cannot use "full" here, we need to know if the container is alive
38
+ const stdin = new PassThrough();
39
+
40
+ const paramsWithConvertedPaths = parameters.map((item) => {
41
+ if (item.startsWith('-')) {
42
+ return item;
43
+ }
44
+ if (item.includes(path.sep) && (fileExists(item) || dirExists(item))) {
45
+ return toPosixPath(
46
+ path.isAbsolute(item) ? path.relative(process.cwd(), item) : item
47
+ );
48
+ }
49
+ return item;
50
+ });
51
+
52
+ const tryCmd = (libdragonInfo, opts = {}) => {
53
+ const enableTTY = Boolean(process.stdout.isTTY && process.stdin.isTTY);
54
+
55
+ return (
56
+ libdragonInfo.containerId &&
57
+ dockerExec(
58
+ libdragonInfo,
59
+ [
60
+ ...dockerRelativeWorkdirParams(libdragonInfo),
61
+ ...dockerHostUserParams(libdragonInfo),
62
+ ],
63
+ [libdragonInfo.options.EXTRA_PARAMS[0], ...paramsWithConvertedPaths],
64
+ {
65
+ userCommand: true,
66
+ // Inherit stdin/out in tandem if we are going to disable TTY o/w the input
67
+ // stream remains inherited by the node process while the output pipe is
68
+ // waiting data from stdout and it behaves like we are still controlling
69
+ // the spawned process while the terminal is actually displaying say for
70
+ // example `less`.
71
+ inheritStdout: enableTTY,
72
+ inheritStdin: enableTTY,
73
+ // spawnProcess defaults does not apply to dockerExec so we need to
74
+ // provide these explicitly here.
75
+ inheritStderr: true,
76
+ ...opts,
77
+ }
78
+ )
43
79
  );
80
+ };
44
81
 
45
82
  let started = false;
46
- const startOnceAndCmd = async () => {
83
+ const startOnceAndCmd = async (stdin) => {
47
84
  if (!started) {
48
- const newId = await start(libdragonInfo);
85
+ const newId = await start(info);
86
+ const newInfo = {
87
+ ...info,
88
+ containerId: newId,
89
+ };
49
90
  started = true;
50
91
 
51
92
  // Re-install vendors on new container if one was created upon start
52
93
  // Ideally we would want the consumer to handle dependencies and rebuild
53
94
  // libdragon if necessary. Currently this saves the day with a little bit
54
95
  // extra waiting when the container is deleted.
55
- if (libdragonInfo.containerId !== newId) {
56
- await installDependencies({
57
- ...libdragonInfo,
58
- containerId: newId,
59
- });
96
+ if (info.containerId !== newId) {
97
+ await installDependencies(newInfo);
60
98
  }
61
- await tryCmd({
62
- ...libdragonInfo,
63
- containerId: newId,
64
- });
99
+ await tryCmd(newInfo, { stdin });
65
100
  return newId;
66
101
  }
67
102
  };
68
103
 
69
- if (!libdragonInfo.containerId) {
104
+ if (!info.containerId) {
70
105
  log(`Container does not exist for sure, restart`, true);
71
106
  await startOnceAndCmd();
72
- return libdragonInfo;
107
+ return info;
73
108
  }
74
109
 
75
110
  try {
76
- await tryCmd(libdragonInfo);
111
+ // Start collecting stdin data on an auxiliary stream such that we can pipe
112
+ // it back to the container process if this fails the first time. Then the
113
+ // initial failed docker process would eat up the input stream. Here, we pass
114
+ // it to the target process eventually via startOnceAndCmd. If the input
115
+ // stream is from a TTY, spawnProcess will already inherit it. Listening
116
+ // to the stream here causes problems for unknown reasons.
117
+ !process.stdin.isTTY && process.stdin.pipe(stdin);
118
+ await tryCmd(info, {
119
+ // Disable the error tty to be able to read the error message in case
120
+ // the container is not running
121
+ inheritStderr: false,
122
+ // In the first run, pass the stdin to the process if it is not a TTY
123
+ // o/w we loose a user input unnecesarily somehow.
124
+ stdin: !process.stdin.isTTY && process.stdin,
125
+ });
77
126
  } catch (e) {
78
127
  if (
79
128
  !e.out ||
80
129
  // TODO: is there a better way?
81
- !e.out.toString().includes(libdragonInfo.containerId)
130
+ !e.out.toString().includes(info.containerId)
82
131
  ) {
83
132
  throw e;
84
133
  }
85
- await startOnceAndCmd();
134
+ await startOnceAndCmd(stdin);
86
135
  }
87
- return libdragonInfo;
136
+ return info;
88
137
  };
89
138
 
90
139
  module.exports = {
@@ -1,9 +1,9 @@
1
1
  const chalk = require('chalk');
2
2
  const commandLineUsage = require('command-line-usage');
3
3
 
4
- const { log } = require('../helpers');
4
+ const { print } = require('../helpers');
5
5
 
6
- const printUsage = (_, actionArr) => {
6
+ const printUsage = (info) => {
7
7
  const actions = require('./');
8
8
  const globalOptionDefinitions = [
9
9
  {
@@ -42,22 +42,24 @@ const printUsage = (_, actionArr) => {
42
42
  You can always switch to manual by re-running \`init\` with \`--strategy manual\`, though you will be responsible for managing the existing submodule/subtree. Also it is not possible to automatically switch back.
43
43
 
44
44
  With the \`manual\` strategy, it is still recommended to have a git repository at project root such that container actions can execute faster by caching the container id inside the \`.git\` folder.\n`,
45
- alias: 'v',
45
+ alias: 's',
46
46
  typeLabel: '<strategy>',
47
47
  group: 'vendoring',
48
48
  },
49
49
  ];
50
50
 
51
- const actionsToShow = actionArr
52
- ?.filter((action) => Object.keys(actions).includes(action))
53
- .filter((action) => !['help'].includes(action));
51
+ const actionsToShow =
52
+ info?.options.EXTRA_PARAMS?.filter(
53
+ (action) =>
54
+ Object.keys(actions).includes(action) && !['help'].includes(action)
55
+ ) ?? (info ? [info.options.CURRENT_ACTION.name] : []);
54
56
 
55
57
  const sections = [
56
58
  {
57
59
  header: chalk.green('Usage:'),
58
60
  content: `libdragon [flags] <action>
59
61
 
60
- For string flags valid syntax are: \`-i <value>\` or \`--image=<value>\``,
62
+ Joining flags not supported, provide them separately like \`-v -i\` `,
61
63
  },
62
64
  ...(actionsToShow?.length
63
65
  ? actionsToShow.flatMap((action) => [
@@ -91,14 +93,14 @@ const printUsage = (_, actionArr) => {
91
93
  },
92
94
  ];
93
95
  const usage = commandLineUsage(sections);
94
- log(usage);
96
+ print(usage);
95
97
  };
96
98
 
97
99
  module.exports = {
98
100
  name: 'help',
99
101
  fn: printUsage,
100
- showStatus: true,
101
102
  forwardsRestParams: true,
103
+ mustHaveProject: false,
102
104
  usage: {
103
105
  name: 'help [action]',
104
106
  summary: 'Display this help information or details for the given action.',
@@ -1,17 +1,16 @@
1
1
  module.exports = {
2
- start: require('./start'),
3
- stop: require('./stop'),
4
2
  init: require('./init'),
5
-
6
- exec: require('./exec'),
7
3
  make: require('./make'),
8
4
 
5
+ update: require('./update'),
6
+ exec: require('./exec'),
9
7
  disasm: require('./disasm'),
10
-
11
8
  install: require('./install'),
12
- update: require('./update'),
9
+
10
+ start: require('./start'),
11
+ stop: require('./stop'),
13
12
 
14
13
  help: require('./help'),
15
14
  version: require('./version'),
16
- clean: require('./clean'),
15
+ destroy: require('./destroy'),
17
16
  };