libdragon 10.4.2 → 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,5 +1,33 @@
1
1
  # Change Log
2
2
 
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
+
3
31
  ## [10.4.2] - 2022-04-03
4
32
 
5
33
  ### Fixed
package/index.js CHANGED
@@ -15,6 +15,7 @@ const {
15
15
  CommandError,
16
16
  ParameterError,
17
17
  ValidationError,
18
+ log,
18
19
  } = require('./modules/helpers');
19
20
  const { readProjectInfo, writeProjectInfo } = require('./modules/project-info');
20
21
 
@@ -33,6 +34,7 @@ for (let i = 2; i < process.argv.length; i++) {
33
34
  continue;
34
35
  }
35
36
 
37
+ // TODO: we might move these to actions as well.
36
38
  if (['--image', '-i'].includes(val)) {
37
39
  options.DOCKER_IMAGE = process.argv[++i];
38
40
  continue;
@@ -57,6 +59,14 @@ for (let i = 2; i < process.argv.length; i++) {
57
59
  continue;
58
60
  }
59
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
+
60
70
  if (val.indexOf('--') >= 0) {
61
71
  console.error(chalk.red(`Invalid flag \`${val}\``));
62
72
  printUsage();
@@ -115,6 +125,12 @@ if (
115
125
  process.exit(STATUS_BAD_PARAM);
116
126
  }
117
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
+
118
134
  readProjectInfo()
119
135
  .then((info) =>
120
136
  currentAction.fn(
@@ -160,10 +176,9 @@ readProjectInfo()
160
176
  // We don't have a user targeted error anymore, we did a mistake for sure
161
177
  process.exit(STATUS_ERROR);
162
178
  })
163
- .then(() => {
164
- // Everything was done, update the configuration file if not exiting early
165
- return writeProjectInfo();
166
- })
179
+ // Everything was done, update the configuration file if not exiting early
180
+ .then(writeProjectInfo)
167
181
  .finally(() => {
182
+ log(`Took: ${process.uptime()}s`, true);
168
183
  process.exit(STATUS_OK);
169
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
+ };
@@ -4,11 +4,8 @@ const { CONTAINER_TARGET_PATH } = require('../constants');
4
4
  const { log, dockerExec, toPosixPath } = require('../helpers');
5
5
 
6
6
  const { start } = require('./start');
7
- const {
8
- dockerHostUserParams,
9
- installDependencies,
10
- mustHaveProject,
11
- } = require('./utils');
7
+ const { dockerHostUserParams } = require('./docker-utils');
8
+ const { installDependencies, mustHaveProject } = require('./utils');
12
9
 
13
10
  function dockerRelativeWorkdir(libdragonInfo) {
14
11
  return (
@@ -72,7 +69,7 @@ const exec = async (libdragonInfo, commandAndParams) => {
72
69
  if (!libdragonInfo.containerId) {
73
70
  log(`Container does not exist for sure, restart`, true);
74
71
  await startOnceAndCmd();
75
- return;
72
+ return libdragonInfo;
76
73
  }
77
74
 
78
75
  try {
@@ -87,6 +84,7 @@ const exec = async (libdragonInfo, commandAndParams) => {
87
84
  }
88
85
  await startOnceAndCmd();
89
86
  }
87
+ return libdragonInfo;
90
88
  };
91
89
 
92
90
  module.exports = {
@@ -94,4 +92,15 @@ module.exports = {
94
92
  fn: exec,
95
93
  forwardsRestParams: true,
96
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
+ },
97
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',
@@ -48,72 +48,6 @@ const printUsage = (_, actionArr) => {
48
48
  },
49
49
  ];
50
50
 
51
- // TODO: move these to respective actions
52
-
53
- const actions = {
54
- help: {
55
- name: 'help [action]',
56
- summary: 'Display this help information or details for the given action.',
57
- },
58
- init: {
59
- name: 'init',
60
- summary: 'Create a libdragon project in the current directory.',
61
- 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.
62
-
63
- 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.
64
-
65
- 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.`,
66
- group: ['docker', 'vendoring'],
67
- },
68
- make: {
69
- name: 'make [params]',
70
- summary: 'Run the libdragon build system in the current directory.',
71
- description: `Runs the libdragon build system in the current directory. It will mirror your current working directory to the container.
72
-
73
- Must be run in an initialized libdragon project. This action is a shortcut to the \`exec\` action under the hood.`,
74
- },
75
- exec: {
76
- name: 'exec <command>',
77
- summary: 'Execute given command in the current directory.',
78
- 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.
79
-
80
- 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.
81
-
82
- 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.
83
-
84
- Must be run in an initialized libdragon project.`,
85
- },
86
- start: {
87
- name: 'start',
88
- summary: 'Start the container for current project.',
89
- description: `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.
90
-
91
- Must be run in an initialized libdragon project.`,
92
- },
93
- name: {
94
- name: 'stop',
95
- summary: 'Stop the container for current project.',
96
- description: `Stop the container assigned to the current libdragon project.
97
-
98
- Must be run in an initialized libdragon project.`,
99
- },
100
- install: {
101
- name: 'install',
102
- summary: 'Vendor libdragon as is.',
103
- 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.
104
-
105
- Must be run in an initialized libdragon project. This can be useful to recover from a half-baked container.`,
106
- },
107
- update: {
108
- name: 'update',
109
- summary: 'Update libdragon and do an install.',
110
- description: `Will update the docker image and if you are using auto-vendoring (see \`--strategy\`), will also update the submodule/subtree from the remote branch (\`trunk\`) with a merge/squash strategy and then perform a \`libdragon install\`. You can use the \`install\` action to only update all libdragon related artifacts in the container.
111
-
112
- Must be run in an initialized libdragon project.`,
113
- group: ['docker'],
114
- },
115
- };
116
-
117
51
  const actionsToShow = actionArr
118
52
  ?.filter((action) => Object.keys(actions).includes(action))
119
53
  .filter((action) => !['help'].includes(action));
@@ -121,28 +55,33 @@ const printUsage = (_, actionArr) => {
121
55
  const sections = [
122
56
  {
123
57
  header: chalk.green('Usage:'),
124
- content: 'libdragon [flags] <action>',
58
+ content: `libdragon [flags] <action>
59
+
60
+ For string flags valid syntax are: \`-i <value>\` or \`--image=<value>\``,
125
61
  },
126
62
  ...(actionsToShow?.length
127
63
  ? actionsToShow.flatMap((action) => [
128
64
  {
129
65
  header: chalk.green(`${action} action:`),
130
- content: actions[action].description,
66
+ content: actions[action].usage.description,
131
67
  },
132
- actions[action].group
68
+ actions[action].usage.group || actions[action].usage.optionList
133
69
  ? {
134
70
  header: `accepted flags:`,
135
- optionList: optionDefinitions,
136
- group: actions[action].group,
71
+ optionList: [
72
+ ...(actions[action].usage.group ? optionDefinitions : []),
73
+ ...(actions[action].usage.optionList ?? []),
74
+ ],
75
+ group: actions[action].usage.group,
137
76
  }
138
77
  : {},
139
78
  ])
140
79
  : [
141
80
  {
142
81
  header: chalk.green('Available Commands:'),
143
- content: Object.values(actions).map((action) => ({
144
- name: action.name,
145
- summary: action.summary,
82
+ content: Object.values(actions).map(({ usage }) => ({
83
+ name: usage.name,
84
+ summary: usage.summary,
146
85
  })),
147
86
  },
148
87
  ]),
@@ -160,4 +99,8 @@ module.exports = {
160
99
  fn: printUsage,
161
100
  showStatus: true,
162
101
  forwardsRestParams: true,
102
+ usage: {
103
+ name: 'help [action]',
104
+ summary: 'Display this help information or details for the given action.',
105
+ },
163
106
  };
@@ -1,25 +1,17 @@
1
- const { fn: exec } = require('./exec');
2
-
3
- const make = async (libdragonInfo, params) => {
4
- await exec(libdragonInfo, ['make', ...params]);
5
- };
6
-
7
- // TODO: separate into files
8
1
  module.exports = {
9
2
  start: require('./start'),
10
3
  stop: require('./stop'),
11
4
  init: require('./init'),
12
5
 
13
6
  exec: require('./exec'),
14
- make: {
15
- name: 'make',
16
- fn: make,
17
- forwardsRestParams: true,
18
- showStatus: true,
19
- },
7
+ make: require('./make'),
8
+
9
+ disasm: require('./disasm'),
20
10
 
21
11
  install: require('./install'),
22
12
  update: require('./update'),
23
13
 
24
14
  help: require('./help'),
15
+ version: require('./version'),
16
+ clean: require('./clean'),
25
17
  };
@@ -24,8 +24,9 @@ const {
24
24
  CommandError,
25
25
  ParameterError,
26
26
  ValidationError,
27
+ toPosixPath,
28
+ toNativePath,
27
29
  } = require('../helpers');
28
- const { setProjectInfoToSave } = require('../project-info');
29
30
 
30
31
  const autoVendor = async (libdragonInfo) => {
31
32
  await runGitMaybeHost(libdragonInfo, ['init']);
@@ -111,25 +112,24 @@ async function init(libdragonInfo) {
111
112
 
112
113
  // Update the strategy information for the project if the flag is provided
113
114
  if (newInfo.options.VENDOR_STRAT) {
114
- newInfo = setProjectInfoToSave({
115
- ...newInfo,
116
- vendorStrategy: newInfo.options.VENDOR_STRAT,
117
- });
115
+ newInfo.vendorStrategy = newInfo.options.VENDOR_STRAT;
118
116
  }
119
117
 
120
118
  // Update the directory information for the project if the flag is provided
121
119
  if (newInfo.options.VENDOR_DIR) {
120
+ const relativeVendorDir = path.relative(
121
+ libdragonInfo.root,
122
+ libdragonInfo.options.VENDOR_DIR
123
+ );
122
124
  // Validate vendoring path
123
- if (path.isAbsolute(newInfo.options.VENDOR_DIR)) {
125
+ if (relativeVendorDir.startsWith('..')) {
124
126
  throw new ParameterError(
125
- '`--directory` must be project-relative as it will be mounted to the docker container.'
127
+ `\`--directory=${libdragonInfo.options.VENDOR_DIR}\` is outside the project directory.`
126
128
  );
127
129
  }
128
130
 
129
- newInfo = setProjectInfoToSave({
130
- ...newInfo,
131
- vendorDirectory: newInfo.options.VENDOR_DIR,
132
- });
131
+ // Immeditately convert it to a posix and relative path
132
+ newInfo.vendorDirectory = toPosixPath(relativeVendorDir);
133
133
  }
134
134
 
135
135
  if (newInfo.haveProjectConfig) {
@@ -145,12 +145,11 @@ async function init(libdragonInfo) {
145
145
  );
146
146
  }
147
147
  // TODO: we may make sure git and submodule is initialized here
148
- await install(newInfo);
149
- return;
148
+ return await install(newInfo);
150
149
  }
151
150
 
152
- newInfo.imageName =
153
- (await updateImage(newInfo, newInfo.imageName)) || newInfo.imageName;
151
+ await updateImage(newInfo, newInfo.imageName);
152
+
154
153
  // Download image and start it
155
154
  const containerReadyPromise = start(newInfo, true).then((newId) => ({
156
155
  ...newInfo,
@@ -159,7 +158,10 @@ async function init(libdragonInfo) {
159
158
 
160
159
  let vendorAndGitReadyPromise = containerReadyPromise;
161
160
  if (newInfo.vendorStrategy !== 'manual') {
162
- const vendorTarget = path.relative(newInfo.root, newInfo.vendorDirectory);
161
+ const vendorTarget = path.relative(
162
+ newInfo.root,
163
+ toNativePath(newInfo.vendorDirectory)
164
+ );
163
165
  const [vendorTargetExists] = await Promise.all([
164
166
  fs.stat(vendorTarget).catch((e) => {
165
167
  if (e.code !== 'ENOENT') throw e;
@@ -171,8 +173,7 @@ async function init(libdragonInfo) {
171
173
  if (vendorTargetExists) {
172
174
  throw new ValidationError(
173
175
  `${path.resolve(
174
- newInfo.root,
175
- newInfo.vendorDirectory
176
+ vendorTarget
176
177
  )} already exists. That is the libdragon vendoring target, please remove and retry.`
177
178
  );
178
179
  }
@@ -192,10 +193,21 @@ async function init(libdragonInfo) {
192
193
  ]);
193
194
 
194
195
  log(chalk.green(`libdragon ready at \`${newInfo.root}\`.`));
196
+ return newInfo;
195
197
  }
196
198
 
197
199
  module.exports = {
198
200
  name: 'init',
199
201
  fn: init,
200
202
  showStatus: true,
203
+ usage: {
204
+ name: 'init',
205
+ summary: 'Create a libdragon project in the current directory.',
206
+ 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.
207
+
208
+ 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.
209
+
210
+ 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.`,
211
+ group: ['docker', 'vendoring'],
212
+ },
201
213
  };
@@ -39,10 +39,18 @@ const install = async (libdragonInfo, skipUpdate) => {
39
39
  // Re-install vendors on new image
40
40
  // TODO: skip this if unnecessary
41
41
  await installDependencies(updatedInfo);
42
+ return updatedInfo;
42
43
  };
43
44
 
44
45
  module.exports = {
45
46
  name: 'install',
46
47
  fn: install,
47
48
  showStatus: true,
49
+ usage: {
50
+ name: 'install',
51
+ summary: 'Vendor libdragon as is.',
52
+ 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.
53
+
54
+ Must be run in an initialized libdragon project. This can be useful to recover from a half-baked container.`,
55
+ },
48
56
  };
@@ -0,0 +1,19 @@
1
+ const { fn: exec } = require('./exec');
2
+
3
+ const make = async (libdragonInfo, params) => {
4
+ return await exec(libdragonInfo, ['make', ...params]);
5
+ };
6
+
7
+ module.exports = {
8
+ name: 'make',
9
+ fn: make,
10
+ forwardsRestParams: true,
11
+ showStatus: true,
12
+ usage: {
13
+ name: 'make [params]',
14
+ summary: 'Run the libdragon build system in the current directory.',
15
+ description: `Runs the libdragon build system in the current directory. It will mirror your current working directory to the container.
16
+
17
+ Must be run in an initialized libdragon project. This action is a shortcut to the \`exec\` action under the hood.`,
18
+ },
19
+ };
@@ -0,0 +1,122 @@
1
+ const path = require('path');
2
+ const fsClassic = require('fs');
3
+
4
+ const _ = require('lodash');
5
+
6
+ const { dockerHostUserParams } = require('./docker-utils');
7
+
8
+ const { CONTAINER_TARGET_PATH } = require('../constants');
9
+ const {
10
+ fileExists,
11
+ toPosixPath,
12
+ spawnProcess,
13
+ dockerExec,
14
+ ValidationError,
15
+ } = require('../helpers');
16
+
17
+ async function findNPMRoot() {
18
+ try {
19
+ const root = path.resolve((await runNPM(['root'])).trim(), '..');
20
+ // Only report if package.json really exists. npm fallbacks to cwd
21
+ if (await fileExists(path.join(root, 'package.json'))) {
22
+ return root;
23
+ }
24
+ } catch {
25
+ // User does not have and does not care about NPM if it didn't work
26
+ return undefined;
27
+ }
28
+ }
29
+
30
+ // Install other NPM dependencies if this is an NPM project
31
+ const installNPMDependencies = async (libdragonInfo) => {
32
+ const npmRoot = await findNPMRoot();
33
+ if (npmRoot) {
34
+ const packageJsonPath = path.join(npmRoot, 'package.json');
35
+
36
+ const { dependencies, devDependencies } = require(packageJsonPath);
37
+
38
+ const deps = await Promise.all(
39
+ Object.keys({
40
+ ...dependencies,
41
+ ...devDependencies,
42
+ })
43
+ .filter((dep) => dep !== 'libdragon')
44
+ .map(async (dep) => {
45
+ const npmPath = await runNPM(['ls', dep, '--parseable=true']);
46
+ return {
47
+ name: dep,
48
+ paths: _.uniq(npmPath.split('\n').filter((f) => f)),
49
+ };
50
+ })
51
+ );
52
+
53
+ await Promise.all(
54
+ deps.map(({ name, paths }) => {
55
+ return new Promise((resolve, reject) => {
56
+ fsClassic.access(
57
+ path.join(paths[0], 'Makefile'),
58
+ fsClassic.F_OK,
59
+ async (e) => {
60
+ if (e) {
61
+ // File does not exist - skip
62
+ resolve();
63
+ return;
64
+ }
65
+
66
+ if (paths.length > 1) {
67
+ reject(
68
+ new ValidationError(
69
+ `Using same dependency with different versions is not supported! ${name}`
70
+ )
71
+ );
72
+ return;
73
+ }
74
+
75
+ try {
76
+ const relativePath = toPosixPath(
77
+ path.relative(libdragonInfo.root, paths[0])
78
+ );
79
+ const containerPath = path.posix.join(
80
+ CONTAINER_TARGET_PATH,
81
+ relativePath
82
+ );
83
+ const makePath = path.posix.join(containerPath, 'Makefile');
84
+
85
+ await dockerExec(
86
+ libdragonInfo,
87
+ [...dockerHostUserParams(libdragonInfo)],
88
+ [
89
+ '/bin/bash',
90
+ '-c',
91
+ '[ -f ' +
92
+ makePath +
93
+ ' ] && make -C ' +
94
+ containerPath +
95
+ ' && make -C ' +
96
+ containerPath +
97
+ ' install',
98
+ ]
99
+ );
100
+
101
+ resolve();
102
+ } catch (e) {
103
+ reject(e);
104
+ }
105
+ }
106
+ );
107
+ });
108
+ })
109
+ );
110
+ }
111
+ };
112
+
113
+ function runNPM(params) {
114
+ return spawnProcess(
115
+ /^win/.test(process.platform) ? 'npm.cmd' : 'npm',
116
+ params
117
+ );
118
+ }
119
+ module.exports = {
120
+ installNPMDependencies,
121
+ findNPMRoot,
122
+ };
@@ -2,7 +2,6 @@ const chalk = require('chalk');
2
2
 
3
3
  const { CONTAINER_TARGET_PATH } = require('../constants');
4
4
  const { spawnProcess, log, dockerExec } = require('../helpers');
5
- const { setProjectInfoToSave } = require('../project-info');
6
5
 
7
6
  const {
8
7
  checkContainerAndClean,
@@ -76,8 +75,6 @@ const initContainer = async (libdragonInfo) => {
76
75
  chalk.green(`Successfully initialized docker container: ${name.trim()}`)
77
76
  );
78
77
 
79
- // Schedule an update to write image name
80
- setProjectInfoToSave(libdragonInfo);
81
78
  return newId;
82
79
  };
83
80
 
@@ -108,7 +105,18 @@ const start = async (libdragonInfo, skipProjectCheck) => {
108
105
 
109
106
  module.exports = {
110
107
  name: 'start',
111
- fn: async (libdragonInfo) => log(await start(libdragonInfo)),
108
+ fn: async (libdragonInfo) => {
109
+ const containerId = await start(libdragonInfo);
110
+ log(containerId);
111
+ return { ...libdragonInfo, containerId };
112
+ },
112
113
  start,
113
114
  showStatus: false, // This will only print out the id
115
+ usage: {
116
+ name: 'start',
117
+ summary: 'Start the container for current project.',
118
+ description: `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.
119
+
120
+ Must be run in an initialized libdragon project.`,
121
+ },
114
122
  };
@@ -12,10 +12,18 @@ const stop = async (libdragonInfo) => {
12
12
  }
13
13
 
14
14
  await spawnProcess('docker', ['container', 'stop', running]);
15
+ return libdragonInfo;
15
16
  };
16
17
 
17
18
  module.exports = {
18
19
  name: 'stop',
19
20
  fn: stop,
20
21
  showStatus: false, // This will only print out the id
22
+ usage: {
23
+ name: 'stop',
24
+ summary: 'Stop the container for current project.',
25
+ description: `Stop the container assigned to the current libdragon project.
26
+
27
+ Must be run in an initialized libdragon project.`,
28
+ },
21
29
  };
@@ -1,5 +1,3 @@
1
- const path = require('path');
2
-
3
1
  const { log } = require('../helpers');
4
2
  const { LIBDRAGON_GIT, LIBDRAGON_BRANCH } = require('../constants');
5
3
  const { runGitMaybeHost, mustHaveProject } = require('./utils');
@@ -27,7 +25,7 @@ const update = async (libdragonInfo) => {
27
25
  'subtree',
28
26
  'pull',
29
27
  '--prefix',
30
- path.relative(libdragonInfo.root, libdragonInfo.vendorDirectory),
28
+ libdragonInfo.vendorDirectory,
31
29
  LIBDRAGON_GIT,
32
30
  LIBDRAGON_BRANCH,
33
31
  '--squash',
@@ -36,11 +34,19 @@ const update = async (libdragonInfo) => {
36
34
 
37
35
  // The second parameter forces it to skip the image update step as we already
38
36
  // do that above.
39
- await install(newInfo, true);
37
+ return await install(newInfo, true);
40
38
  };
41
39
 
42
40
  module.exports = {
43
41
  name: 'update',
44
42
  fn: update,
45
43
  showStatus: true,
44
+ usage: {
45
+ name: 'update',
46
+ summary: 'Update libdragon and do an install.',
47
+ description: `Will update the docker image and if you are using auto-vendoring (see \`--strategy\`), will also update the submodule/subtree from the remote branch (\`trunk\`) with a merge/squash strategy and then perform a \`libdragon install\`. You can use the \`install\` action to only update all libdragon related artifacts in the container.
48
+
49
+ Must be run in an initialized libdragon project.`,
50
+ group: ['docker'],
51
+ },
46
52
  };
@@ -1,9 +1,6 @@
1
1
  const path = require('path');
2
- const fsClassic = require('fs');
3
2
  const fs = require('fs/promises');
4
3
 
5
- const _ = require('lodash');
6
-
7
4
  const {
8
5
  CONTAINER_TARGET_PATH,
9
6
  CACHED_CONTAINER_FILE,
@@ -12,7 +9,6 @@ const {
12
9
  const {
13
10
  fileExists,
14
11
  log,
15
- toPosixPath,
16
12
  spawnProcess,
17
13
  dockerExec,
18
14
  dirExists,
@@ -20,30 +16,16 @@ const {
20
16
  CommandError,
21
17
  ValidationError,
22
18
  ParameterError,
19
+ toNativePath,
23
20
  } = require('../helpers');
24
21
 
25
- function dockerHostUserParams(libdragonInfo) {
26
- const { uid, gid } = libdragonInfo.userInfo;
27
- return ['-u', `${uid >= 0 ? uid : ''}:${gid >= 0 ? gid : ''}`];
28
- }
29
-
30
- async function findNPMRoot() {
31
- try {
32
- const root = path.resolve((await runNPM(['root'])).trim(), '..');
33
- // Only report if package.json really exists. npm fallbacks to cwd
34
- if (await fileExists(path.join(root, 'package.json'))) {
35
- return root;
36
- }
37
- } catch {
38
- // User does not have and does not care about NPM if it didn't work
39
- return undefined;
40
- }
41
- }
22
+ const { dockerHostUserParams } = require('./docker-utils');
23
+ const { installNPMDependencies } = require('./npm-utils');
42
24
 
43
25
  const installDependencies = async (libdragonInfo) => {
44
26
  const buildScriptPath = path.join(
45
27
  libdragonInfo.root,
46
- libdragonInfo.vendorDirectory,
28
+ toNativePath(libdragonInfo.vendorDirectory),
47
29
  'build.sh'
48
30
  );
49
31
  if (!(await fileExists(buildScriptPath))) {
@@ -58,96 +40,13 @@ const installDependencies = async (libdragonInfo) => {
58
40
  libdragonInfo,
59
41
  [
60
42
  '--workdir',
61
- CONTAINER_TARGET_PATH +
62
- '/' +
63
- toPosixPath(
64
- path.relative(libdragonInfo.root, libdragonInfo.vendorDirectory)
65
- ),
43
+ CONTAINER_TARGET_PATH + '/' + libdragonInfo.vendorDirectory,
66
44
  ...dockerHostUserParams(libdragonInfo),
67
45
  ],
68
46
  ['/bin/bash', './build.sh']
69
47
  );
70
48
 
71
- // Install other NPM dependencies if this is an NPM project
72
- const npmRoot = await findNPMRoot();
73
- if (npmRoot) {
74
- const packageJsonPath = path.join(npmRoot, 'package.json');
75
-
76
- const { dependencies, devDependencies } = require(packageJsonPath);
77
-
78
- const deps = await Promise.all(
79
- Object.keys({
80
- ...dependencies,
81
- ...devDependencies,
82
- })
83
- .filter((dep) => dep !== 'libdragon')
84
- .map(async (dep) => {
85
- const npmPath = await runNPM(['ls', dep, '--parseable=true']);
86
- return {
87
- name: dep,
88
- paths: _.uniq(npmPath.split('\n').filter((f) => f)),
89
- };
90
- })
91
- );
92
-
93
- await Promise.all(
94
- deps.map(({ name, paths }) => {
95
- return new Promise((resolve, reject) => {
96
- fsClassic.access(
97
- path.join(paths[0], 'Makefile'),
98
- fsClassic.F_OK,
99
- async (e) => {
100
- if (e) {
101
- // File does not exist - skip
102
- resolve();
103
- return;
104
- }
105
-
106
- if (paths.length > 1) {
107
- reject(
108
- new ValidationError(
109
- `Using same dependency with different versions is not supported! ${name}`
110
- )
111
- );
112
- return;
113
- }
114
-
115
- try {
116
- const relativePath = toPosixPath(
117
- path.relative(libdragonInfo.root, paths[0])
118
- );
119
- const containerPath = path.posix.join(
120
- CONTAINER_TARGET_PATH,
121
- relativePath
122
- );
123
- const makePath = path.posix.join(containerPath, 'Makefile');
124
-
125
- await dockerExec(
126
- libdragonInfo,
127
- [...dockerHostUserParams(libdragonInfo)],
128
- [
129
- '/bin/bash',
130
- '-c',
131
- '[ -f ' +
132
- makePath +
133
- ' ] && make -C ' +
134
- containerPath +
135
- ' && make -C ' +
136
- containerPath +
137
- ' install',
138
- ]
139
- );
140
-
141
- resolve();
142
- } catch (e) {
143
- reject(e);
144
- }
145
- }
146
- );
147
- });
148
- })
149
- );
150
- }
49
+ await installNPMDependencies(libdragonInfo);
151
50
  };
152
51
 
153
52
  /**
@@ -249,13 +148,6 @@ async function runGitMaybeHost(libdragonInfo, params, interactive = 'full') {
249
148
  }
250
149
  }
251
150
 
252
- function runNPM(params) {
253
- return spawnProcess(
254
- /^win/.test(process.platform) ? 'npm.cmd' : 'npm',
255
- params
256
- );
257
- }
258
-
259
151
  async function checkContainerAndClean(libdragonInfo) {
260
152
  const id =
261
153
  libdragonInfo.containerId &&
@@ -319,9 +211,7 @@ module.exports = {
319
211
  destroyContainer,
320
212
  checkContainerRunning,
321
213
  checkContainerAndClean,
322
- dockerHostUserParams,
323
214
  tryCacheContainerId,
324
215
  runGitMaybeHost,
325
- findNPMRoot,
326
216
  mustHaveProject,
327
217
  };
@@ -0,0 +1,17 @@
1
+ const { version } = require('../../package.json');
2
+ const { log } = require('../helpers');
3
+
4
+ const printVersion = async () => {
5
+ log(`libdragon-cli v${version}`);
6
+ };
7
+
8
+ module.exports = {
9
+ name: 'version',
10
+ fn: printVersion,
11
+ showStatus: false,
12
+ usage: {
13
+ name: 'version',
14
+ summary: 'Display cli version.',
15
+ description: `Displays currently running cli version.`,
16
+ },
17
+ };
@@ -209,7 +209,11 @@ async function copyDirContents(src, dst) {
209
209
  }
210
210
 
211
211
  function toPosixPath(p) {
212
- return p.replace(new RegExp('\\' + path.sep), path.posix.sep);
212
+ return p.replace(new RegExp('\\' + path.sep, 'g'), path.posix.sep);
213
+ }
214
+
215
+ function toNativePath(p) {
216
+ return p.replace(new RegExp('\\' + path.posix.sep, 'g'), path.sep);
213
217
  }
214
218
 
215
219
  function assert(condition, error) {
@@ -236,6 +240,7 @@ function log(text, verboseOnly = false) {
236
240
  module.exports = {
237
241
  spawnProcess,
238
242
  toPosixPath,
243
+ toNativePath,
239
244
  log,
240
245
  dockerExec,
241
246
  assert,
@@ -4,11 +4,12 @@ const fs = require('fs/promises');
4
4
 
5
5
  const {
6
6
  checkContainerAndClean,
7
- findNPMRoot,
8
7
  runGitMaybeHost,
9
8
  tryCacheContainerId,
10
9
  } = require('./actions/utils');
11
10
 
11
+ const { findNPMRoot } = require('./actions/npm-utils');
12
+
12
13
  const {
13
14
  LIBDRAGON_PROJECT_MANIFEST,
14
15
  CONFIG_FILE,
@@ -20,7 +21,13 @@ const {
20
21
  CONTAINER_TARGET_PATH,
21
22
  } = require('./constants');
22
23
 
23
- const { fileExists, log, spawnProcess, toPosixPath } = require('./helpers');
24
+ const {
25
+ fileExists,
26
+ log,
27
+ spawnProcess,
28
+ toPosixPath,
29
+ assert,
30
+ } = require('./helpers');
24
31
 
25
32
  async function findContainerId(libdragonInfo) {
26
33
  const idFile = path.join(libdragonInfo.root, '.git', CACHED_CONTAINER_FILE);
@@ -96,7 +103,7 @@ async function readProjectInfo() {
96
103
  // Set the defaults immediately, these should be present at all times even
97
104
  // if we are migrating from the old config because they did not exist before
98
105
  imageName: DOCKER_HUB_IMAGE,
99
- vendorDirectory: path.join('.', LIBDRAGON_SUBMODULE),
106
+ vendorDirectory: toPosixPath(path.join('.', LIBDRAGON_SUBMODULE)),
100
107
  vendorStrategy: DEFAULT_STRATEGY,
101
108
  };
102
109
 
@@ -158,30 +165,16 @@ async function readProjectInfo() {
158
165
  log(`Active vendor directory: ${info.vendorDirectory}`, true);
159
166
  log(`Active vendor strategy: ${info.vendorStrategy}`, true);
160
167
 
161
- // Cache the latest image name
162
- setProjectInfoToSave(info);
163
168
  return info;
164
169
  }
165
170
 
166
- let projectInfoToWrite = {};
167
171
  /**
168
- * Updates project info to be written. The provided keys are overwritten without
169
- * changing the existing values. When the process exists successfully these will
170
- * get written to the configuration file. Echoes back the given info.
171
172
  * @param info This is only the base info without action properties like showStatus
172
173
  * fn and command line options
173
174
  */
174
- function setProjectInfoToSave(info) {
175
- projectInfoToWrite = { ...projectInfoToWrite, ...info };
176
- return info;
177
- }
178
-
179
- /**
180
- * @param info This is only the base info without action properties like showStatus
181
- * fn and command line options
182
- */
183
- async function writeProjectInfo(info = projectInfoToWrite) {
175
+ async function writeProjectInfo(info) {
184
176
  // Do not log anything here as it may litter the output being always run on exit
177
+ if (!info) return;
185
178
 
186
179
  const projectPath = path.join(info.root, LIBDRAGON_PROJECT_MANIFEST);
187
180
 
@@ -195,13 +188,17 @@ async function writeProjectInfo(info = projectInfoToWrite) {
195
188
  await fs.mkdir(projectPath);
196
189
  }
197
190
 
191
+ assert(
192
+ toPosixPath(info.vendorDirectory) === info.vendorDirectory,
193
+ new Error('vendorDirectory should always be in posix format')
194
+ );
195
+
198
196
  await fs.writeFile(
199
197
  path.join(projectPath, CONFIG_FILE),
200
198
  JSON.stringify(
201
199
  {
202
200
  imageName: info.imageName,
203
- // Always save this in posix format
204
- vendorDirectory: toPosixPath(info.vendorDirectory),
201
+ vendorDirectory: info.vendorDirectory,
205
202
  vendorStrategy: info.vendorStrategy,
206
203
  },
207
204
  null,
@@ -211,4 +208,4 @@ async function writeProjectInfo(info = projectInfoToWrite) {
211
208
  log(`Configuration file updated`, true);
212
209
  }
213
210
 
214
- module.exports = { readProjectInfo, writeProjectInfo, setProjectInfoToSave };
211
+ module.exports = { readProjectInfo, writeProjectInfo };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libdragon",
3
- "version": "10.4.2",
3
+ "version": "10.5.0",
4
4
  "description": "This is a docker wrapper for libdragon",
5
5
  "main": "index.js",
6
6
  "engines": {
package/skeleton/Makefile CHANGED
@@ -3,16 +3,17 @@ SOURCE_DIR=src
3
3
  BUILD_DIR=build
4
4
  include $(N64_INST)/include/n64.mk
5
5
 
6
- src=main.c
7
-
8
6
  all: hello.z64
7
+ .PHONY: all
8
+
9
+ OBJS = $(BUILD_DIR)/main.o
9
10
 
10
11
  hello.z64: N64_ROM_TITLE="Hello World"
11
- $(BUILD_DIR)/hello.elf: $(src:%.c=$(BUILD_DIR)/%.o)
12
12
 
13
- clean:
14
- rm -f $(BUILD_DIR)/* hello.z64
13
+ $(BUILD_DIR)/hello.elf: $(OBJS)
15
14
 
16
- -include $(wildcard $(BUILD_DIR)/*.d)
15
+ clean:
16
+ rm -f $(BUILD_DIR) *.z64
17
+ .PHONY: clean
17
18
 
18
- .PHONY: all clean
19
+ -include $(wildcard $(BUILD_DIR)/*.d)