libdragon 10.4.0 → 10.5.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/CHANGELOG.md CHANGED
@@ -1,6 +1,64 @@
1
1
  # Change Log
2
2
 
3
- ## [10.4.0] - 2022-03-23
3
+ ## [10.5.0] - 2022-04-09
4
+ ### Fixed
5
+
6
+ - Fix a path bug that would cause incorrect behaviour when the command is run
7
+ deeper than a single level in the project folder.
8
+ - Fix a potential issue where `build.sh`might be incorrectly found inexistant
9
+ if the OS is picky about the paths to have native separators.
10
+ - Only save project information when necessary. Previously actions like `help`
11
+ were saving project info mistakenly.
12
+
13
+ ### Added
14
+
15
+ - `disasm` action to simplify disassembling ELF files generated by the toolchain.
16
+ - `version` action to display current version.
17
+ - `clean` action to remove the libdragon project.
18
+ - Additional documentation for flags.
19
+ - Print duration information when verbose.
20
+
21
+ ### Changed
22
+
23
+ - Refactored out NPM related functions.
24
+ - Moved usage parameters to respective actions files as a refactor.
25
+ - It is possible to provide an absolute path to init `--directory` as long as it
26
+ is inside the project directory. Previously it was possible to provide somewhere
27
+ outside the project, but it would fail with an unexpected error.
28
+ - Simplify saving mechanism. Each action now internally resolves into the data
29
+ to save if any.
30
+
31
+ ## [10.4.2] - 2022-04-03
32
+
33
+ ### Fixed
34
+
35
+ - Make sure actions depending on an `init` fail in a non-project directory to
36
+ keep the project state consistent. This fixes #51.
37
+ - `update` action now tries to update the toolchain image as well. Previously
38
+ this was not the case contrary to what someone would expect. Considering it won't
39
+ change the behaviour for non-latest images and the toolchain did not have any
40
+ breaking changes for a long time, this is not considered a breaking change either.
41
+ - `start` action was printing stuff other than the container id. It doesn't
42
+ anymore.
43
+ - Stop unnecessarily printing container id and a few messages related to updates.
44
+ - Fix a potential race condition that might cause unexpected failures.
45
+ - Correct some errors' exit codes.
46
+ ### Added
47
+
48
+ - A new exit code (`4`) to represent unexpected conditions.
49
+
50
+ ### Changed
51
+
52
+ - Deprecated providing the image flag for `install` action by displaying a
53
+ warning and removing it from documentation, without changing behaviour even
54
+ though it is higly unlikely this feature was ever used. It mainly exists for
55
+ historical reasons and it wil be removed in next major release.
56
+ - Update documentation to warn against changing strategy is a one way operation.
57
+ - Update documentation to reflect `update` action changes.
58
+ - Minor refactors.
59
+ - Update submodule for local environment.
60
+
61
+ ## [10.4.1] - 2022-03-23
4
62
 
5
63
  ### Fixed
6
64
 
package/index.js CHANGED
@@ -8,9 +8,15 @@ const {
8
8
  STATUS_OK,
9
9
  STATUS_BAD_PARAM,
10
10
  STATUS_ERROR,
11
+ STATUS_VALIDATION_ERROR,
11
12
  } = require('./modules/constants');
12
13
  const { globals } = require('./modules/globals');
13
- const { CommandError, ParameterError } = require('./modules/helpers');
14
+ const {
15
+ CommandError,
16
+ ParameterError,
17
+ ValidationError,
18
+ log,
19
+ } = require('./modules/helpers');
14
20
  const { readProjectInfo, writeProjectInfo } = require('./modules/project-info');
15
21
 
16
22
  let options = {},
@@ -28,6 +34,7 @@ for (let i = 2; i < process.argv.length; i++) {
28
34
  continue;
29
35
  }
30
36
 
37
+ // TODO: we might move these to actions as well.
31
38
  if (['--image', '-i'].includes(val)) {
32
39
  options.DOCKER_IMAGE = process.argv[++i];
33
40
  continue;
@@ -52,6 +59,14 @@ for (let i = 2; i < process.argv.length; i++) {
52
59
  continue;
53
60
  }
54
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
+
55
70
  if (val.indexOf('--') >= 0) {
56
71
  console.error(chalk.red(`Invalid flag \`${val}\``));
57
72
  printUsage();
@@ -110,6 +125,12 @@ if (
110
125
  process.exit(STATUS_BAD_PARAM);
111
126
  }
112
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
+
113
134
  readProjectInfo()
114
135
  .then((info) =>
115
136
  currentAction.fn(
@@ -127,6 +148,12 @@ readProjectInfo()
127
148
  printUsage(undefined, [currentAction.name]);
128
149
  process.exit(STATUS_BAD_PARAM);
129
150
  }
151
+
152
+ if (e instanceof ValidationError) {
153
+ console.error(chalk.red(e.message));
154
+ process.exit(STATUS_VALIDATION_ERROR);
155
+ }
156
+
130
157
  const userTargetedError = e instanceof CommandError && e.userCommand;
131
158
 
132
159
  // Show additional information to user if verbose or we did a mistake
@@ -149,10 +176,9 @@ readProjectInfo()
149
176
  // We don't have a user targeted error anymore, we did a mistake for sure
150
177
  process.exit(STATUS_ERROR);
151
178
  })
152
- .then(() => {
153
- // Everything was done, update the configuration file if not exiting early
154
- return writeProjectInfo();
155
- })
179
+ // Everything was done, update the configuration file if not exiting early
180
+ .then(writeProjectInfo)
156
181
  .finally(() => {
182
+ log(`Took: ${process.uptime()}s`, true);
157
183
  process.exit(STATUS_OK);
158
184
  });
@@ -0,0 +1,35 @@
1
+ const fs = require('fs/promises');
2
+ const path = require('path');
3
+
4
+ const { mustHaveProject, destroyContainer } = require('./utils');
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);
10
+
11
+ await destroyContainer(libdragonInfo);
12
+
13
+ const projectPath = path.join(libdragonInfo.root, LIBDRAGON_PROJECT_MANIFEST);
14
+ const configPath = path.join(projectPath, CONFIG_FILE);
15
+
16
+ if (await fileExists(configPath)) {
17
+ await fs.rm(configPath);
18
+ }
19
+ if (dirExists(projectPath)) {
20
+ await fs.rmdir(projectPath);
21
+ }
22
+ };
23
+
24
+ module.exports = {
25
+ name: 'clean',
26
+ fn: clean,
27
+ showStatus: true,
28
+ usage: {
29
+ name: 'clean',
30
+ 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.`,
34
+ },
35
+ };
@@ -0,0 +1,116 @@
1
+ const path = require('path');
2
+ const fs = require('fs/promises');
3
+
4
+ const { mustHaveProject } = require('./utils');
5
+ const { fn: exec } = require('./exec');
6
+ const {
7
+ ValidationError,
8
+ toPosixPath,
9
+ dirExists,
10
+ fileExists,
11
+ ParameterError,
12
+ } = require('../helpers');
13
+
14
+ const findElf = async (stop, start = '.') => {
15
+ start = path.resolve(start);
16
+
17
+ const files = await fs.readdir(start);
18
+
19
+ const buildDir = path.join(start, 'build');
20
+ if (await dirExists(buildDir)) {
21
+ const elfFile = await findElf(buildDir, buildDir);
22
+ if (elfFile) return elfFile;
23
+ }
24
+
25
+ const elfFiles = files.filter((name) => name.endsWith('.elf'));
26
+
27
+ if (elfFiles.length > 1) {
28
+ throw new ValidationError(
29
+ `Multiple ELF files found in ${path.resolve(
30
+ start
31
+ )}. Use --file to specify.`
32
+ );
33
+ }
34
+
35
+ if (elfFiles.length === 1) {
36
+ return path.join(start, elfFiles[0]);
37
+ }
38
+
39
+ const parent = path.join(start, '..');
40
+ if (start !== stop) {
41
+ return await findElf(stop, parent);
42
+ } else {
43
+ throw new ValidationError(`No ELF files found. Use --file to specify.`);
44
+ }
45
+ };
46
+
47
+ const disasm = async (libdragonInfo, extraArgs) => {
48
+ await mustHaveProject(libdragonInfo);
49
+
50
+ let elfPath;
51
+ if (libdragonInfo.options.FILE) {
52
+ if (
53
+ path
54
+ .relative(libdragonInfo.root, libdragonInfo.options.FILE)
55
+ .startsWith('..')
56
+ ) {
57
+ throw new ParameterError(
58
+ `Provided file ${libdragonInfo.options.FILE} is outside the project directory.`
59
+ );
60
+ }
61
+ if (!(await fileExists(libdragonInfo.options.FILE)))
62
+ throw new ParameterError(
63
+ `Provided file ${libdragonInfo.options.FILE} does not exist`
64
+ );
65
+ elfPath = libdragonInfo.options.FILE;
66
+ }
67
+ elfPath = elfPath ?? (await findElf(libdragonInfo.root));
68
+
69
+ const haveSymbol = extraArgs.length > 0 && !extraArgs[0].startsWith('-');
70
+
71
+ const finalArgs = haveSymbol
72
+ ? [`--disassemble=${extraArgs[0]}`, ...extraArgs.slice(1)]
73
+ : extraArgs;
74
+
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
+ ]);
84
+ };
85
+
86
+ module.exports = {
87
+ name: 'disasm',
88
+ fn: disasm,
89
+ showStatus: true,
90
+ forwardsRestParams: true,
91
+ usage: {
92
+ name: 'disasm [symbol|flags]',
93
+ summary: 'Disassemble the nearest *.elf file.',
94
+ description: `Executes \`objdump\` for the nearest *.elf file starting from the working directory, going up. If there is a \`build\` directory in the searched paths, checks inside it as well. Any extra flags are passed down to \`objdump\` before the filename.
95
+
96
+ If a symbol name is provided after the action, it is converted to \`--disassembly=<symbol>\` and intermixed source* (\`-S\`) is automatically applied. This allows disassembling a single symbol by running;
97
+
98
+ \`libdragon disasm main\`
99
+
100
+ Again, any following flags are forwarded down. If run without extra symbol/flags, disassembles whole ELF with \`-S\` by default.
101
+
102
+ Must be run in an initialized libdragon project.
103
+
104
+ * Note that to be able to see the source, the code must be built with \`D=1\``,
105
+
106
+ optionList: [
107
+ {
108
+ name: 'file',
109
+ description:
110
+ 'Provide a specific ELF file relative to current working directory and inside the libdragon project.',
111
+ alias: 'f',
112
+ typeLabel: ' ',
113
+ },
114
+ ],
115
+ },
116
+ };
@@ -0,0 +1,8 @@
1
+ function dockerHostUserParams(libdragonInfo) {
2
+ const { uid, gid } = libdragonInfo.userInfo;
3
+ return ['-u', `${uid >= 0 ? uid : ''}:${gid >= 0 ? gid : ''}`];
4
+ }
5
+
6
+ module.exports = {
7
+ dockerHostUserParams,
8
+ };
@@ -3,8 +3,9 @@ const path = require('path');
3
3
  const { CONTAINER_TARGET_PATH } = require('../constants');
4
4
  const { log, dockerExec, toPosixPath } = require('../helpers');
5
5
 
6
- const { fn: start } = require('./start');
7
- const { dockerHostUserParams, installDependencies } = require('./utils');
6
+ const { start } = require('./start');
7
+ const { dockerHostUserParams } = require('./docker-utils');
8
+ const { installDependencies, mustHaveProject } = require('./utils');
8
9
 
9
10
  function dockerRelativeWorkdir(libdragonInfo) {
10
11
  return (
@@ -19,6 +20,8 @@ function dockerRelativeWorkdirParams(libdragonInfo) {
19
20
  }
20
21
 
21
22
  const exec = async (libdragonInfo, commandAndParams) => {
23
+ await mustHaveProject(libdragonInfo);
24
+
22
25
  log(
23
26
  `Running ${commandAndParams[0]} at ${dockerRelativeWorkdir(
24
27
  libdragonInfo
@@ -66,7 +69,7 @@ const exec = async (libdragonInfo, commandAndParams) => {
66
69
  if (!libdragonInfo.containerId) {
67
70
  log(`Container does not exist for sure, restart`, true);
68
71
  await startOnceAndCmd();
69
- return;
72
+ return libdragonInfo;
70
73
  }
71
74
 
72
75
  try {
@@ -81,6 +84,7 @@ const exec = async (libdragonInfo, commandAndParams) => {
81
84
  }
82
85
  await startOnceAndCmd();
83
86
  }
87
+ return libdragonInfo;
84
88
  };
85
89
 
86
90
  module.exports = {
@@ -88,4 +92,15 @@ module.exports = {
88
92
  fn: exec,
89
93
  forwardsRestParams: true,
90
94
  showStatus: true,
95
+ usage: {
96
+ name: 'exec <command>',
97
+ summary: 'Execute given command in the current directory.',
98
+ description: `Executes the given command in the container passing down any arguments provided. If you change your host working directory, the command will be executed in the corresponding folder in the container as well.
99
+
100
+ This action will first try to execute the command in the container and if the container is not accessible, it will attempt a complete \`start\` cycle.
101
+
102
+ This will properly passthrough your TTY if you have one. So by running \`libdragon exec bash\` you can start an interactive bash session with full TTY support.
103
+
104
+ Must be run in an initialized libdragon project.`,
105
+ },
91
106
  };
@@ -4,6 +4,7 @@ const commandLineUsage = require('command-line-usage');
4
4
  const { log } = require('../helpers');
5
5
 
6
6
  const printUsage = (_, actionArr) => {
7
+ const actions = require('./');
7
8
  const globalOptionDefinitions = [
8
9
  {
9
10
  name: 'verbose',
@@ -11,7 +12,6 @@ const printUsage = (_, actionArr) => {
11
12
  'Be verbose. This will print all commands dispatched and their outputs as well. Will also start printing error stack traces.',
12
13
  alias: 'v',
13
14
  typeLabel: ' ',
14
- group: 'global',
15
15
  },
16
16
  ];
17
17
 
@@ -26,7 +26,7 @@ const printUsage = (_, actionArr) => {
26
26
  },
27
27
  {
28
28
  name: 'directory',
29
- description: `Directory where libdragon files are expected. It must be a project relative path as it will be mounted on the docker container. The cli will create and manage it when using a non-manual strategy. Defaults to \`./libdragon\` if not provided.\n`,
29
+ description: `Directory where libdragon files are expected. It must be inside the libdragon project as it will be mounted on the docker container. The cli will create and manage it when using a non-manual strategy. Defaults to \`./libdragon\` if not provided.\n`,
30
30
  alias: 'd',
31
31
  typeLabel: '<path>',
32
32
  group: 'vendoring',
@@ -39,75 +39,15 @@ const printUsage = (_, actionArr) => {
39
39
 
40
40
  To disable auto-vendoring, init with \`manual\`. With \`manual\`, libdragon files are expected at the location provided by \`--directory\` flag and the user is responsible for vendoring and updating them. This will allow using any other manual vendoring method.
41
41
 
42
- 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.
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
- It is also possible to opt-out later on by running \`init\` with \`--strategy manual\`, though you will be responsible for managing the existing submodule/subtree.\n`,
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
45
  alias: 'v',
46
46
  typeLabel: '<strategy>',
47
47
  group: 'vendoring',
48
48
  },
49
49
  ];
50
50
 
51
- const actions = {
52
- help: {
53
- name: 'help [action]',
54
- summary: 'Display this help information or details for the given action.',
55
- },
56
- init: {
57
- name: 'init',
58
- summary: 'Create a libdragon project in the current directory.',
59
- description: `Creates a libdragon project in the current directory. Every libdragon project will have its own docker container instance. If you are in a git repository or an NPM project, libdragon will be initialized at their root also marking there with a \`.libdragon\` folder. Do not remove the \`.libdragon\` folder and commit its contents if you are using source control, as it keeps persistent libdragon project information.
60
-
61
- 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.
62
-
63
- 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.`,
64
- group: ['docker', 'vendoring'],
65
- },
66
- make: {
67
- name: 'make [params]',
68
- summary: 'Run the libdragon build system in the current directory.',
69
- description: `Runs the libdragon build system in the current directory. It will mirror your current working directory to the container.
70
-
71
- This action is a shortcut to the \`exec\` action under the hood.`,
72
- },
73
- exec: {
74
- name: 'exec <command>',
75
- summary: 'Execute given command in the current directory.',
76
- description: `Executes the given command in the container passing down any arguments provided. If you change your host working directory, the command will be executed in the corresponding folder in the container as well.
77
-
78
- This action will first try to execute the command in the container and if the container is not accessible, it will attempt a complete \`start\` cycle.
79
-
80
- This will properly passthrough your TTY if you have one. So by running \`libdragon exec bash\` you can start an interactive bash session with full TTY support.`,
81
- },
82
- start: {
83
- name: 'start',
84
- summary: 'Start the container for current project.',
85
- description:
86
- 'Start the container assigned to the current libdragon project. Will first attempt to start an existing container if found, followed by a new container run and installation similar to the `install` action. Will always print out the container id on success.',
87
- },
88
- name: {
89
- name: 'stop',
90
- summary: 'Stop the container for current project.',
91
- description:
92
- 'Stop the container assigned to the current libdragon project.',
93
- },
94
- install: {
95
- name: 'install',
96
- summary: 'Vendor libdragon as is.',
97
- group: ['docker'],
98
- description: `Attempts to build and install everything libdragon related into the container. This includes all the tools and third parties used by libdragon except for the toolchain. If you have made changes to libdragon, you can execute this action to build everything based on your changes. Requires you to have an intact vendoring target (also see the \`--directory\` flag). If you are not working on libdragon itself, you can just use the \`update\` action instead.
99
-
100
- This can be useful to recover from a half-baked container.`,
101
- },
102
- update: {
103
- name: 'update',
104
- summary: 'Update libdragon and do an install.',
105
- description:
106
- 'If you are using auto-vendoring (see `--strategy`), this action will update the submodule/subtree from the remote branch (`trunk`) with a merge/squash strategy and then perform a `libdragon install`. This is the same as an `install` when the vendoring strategy is `manual`. You can use the `install` action to only update all libdragon related artifacts in the container.',
107
- group: ['docker'],
108
- },
109
- };
110
-
111
51
  const actionsToShow = actionArr
112
52
  ?.filter((action) => Object.keys(actions).includes(action))
113
53
  .filter((action) => !['help'].includes(action));
@@ -115,28 +55,33 @@ const printUsage = (_, actionArr) => {
115
55
  const sections = [
116
56
  {
117
57
  header: chalk.green('Usage:'),
118
- content: 'libdragon [flags] <action>',
58
+ content: `libdragon [flags] <action>
59
+
60
+ For string flags valid syntax are: \`-i <value>\` or \`--image=<value>\``,
119
61
  },
120
62
  ...(actionsToShow?.length
121
63
  ? actionsToShow.flatMap((action) => [
122
64
  {
123
65
  header: chalk.green(`${action} action:`),
124
- content: actions[action].description,
66
+ content: actions[action].usage.description,
125
67
  },
126
- actions[action].group
68
+ actions[action].usage.group || actions[action].usage.optionList
127
69
  ? {
128
70
  header: `accepted flags:`,
129
- optionList: optionDefinitions,
130
- group: actions[action].group,
71
+ optionList: [
72
+ ...(actions[action].usage.group ? optionDefinitions : []),
73
+ ...(actions[action].usage.optionList ?? []),
74
+ ],
75
+ group: actions[action].usage.group,
131
76
  }
132
77
  : {},
133
78
  ])
134
79
  : [
135
80
  {
136
81
  header: chalk.green('Available Commands:'),
137
- content: Object.values(actions).map((action) => ({
138
- name: action.name,
139
- summary: action.summary,
82
+ content: Object.values(actions).map(({ usage }) => ({
83
+ name: usage.name,
84
+ summary: usage.summary,
140
85
  })),
141
86
  },
142
87
  ]),
@@ -154,4 +99,8 @@ module.exports = {
154
99
  fn: printUsage,
155
100
  showStatus: true,
156
101
  forwardsRestParams: true,
102
+ usage: {
103
+ name: 'help [action]',
104
+ summary: 'Display this help information or details for the given action.',
105
+ },
157
106
  };
@@ -1,43 +1,17 @@
1
- const { spawnProcess } = require('../helpers');
2
- const { checkContainerRunning } = require('./utils');
3
-
4
- const { fn: exec } = require('./exec');
5
-
6
- const stop = async (libdragonInfo) => {
7
- const running =
8
- libdragonInfo.containerId &&
9
- (await checkContainerRunning(libdragonInfo.containerId));
10
- if (!running) {
11
- return;
12
- }
13
-
14
- await spawnProcess('docker', ['container', 'stop', running]);
15
- };
16
-
17
- const make = async (libdragonInfo, params) => {
18
- await exec(libdragonInfo, ['make', ...params]);
19
- };
20
-
21
- // TODO: separate into files
22
1
  module.exports = {
23
2
  start: require('./start'),
24
- stop: {
25
- name: 'stop',
26
- fn: stop,
27
- showStatus: false, // This will only print out the id
28
- },
3
+ stop: require('./stop'),
29
4
  init: require('./init'),
30
5
 
31
6
  exec: require('./exec'),
32
- make: {
33
- name: 'make',
34
- fn: make,
35
- forwardsRestParams: true,
36
- showStatus: true,
37
- },
7
+ make: require('./make'),
8
+
9
+ disasm: require('./disasm'),
38
10
 
39
11
  install: require('./install'),
40
12
  update: require('./update'),
41
13
 
42
14
  help: require('./help'),
15
+ version: require('./version'),
16
+ clean: require('./clean'),
43
17
  };