libdragon 10.0.0 → 10.3.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.
@@ -9,6 +9,7 @@ const {
9
9
  LIBDRAGON_PROJECT_MANIFEST,
10
10
  IMAGE_FILE,
11
11
  DOCKER_HUB_IMAGE,
12
+ CONTAINER_TARGET_PATH,
12
13
  } = require('./constants');
13
14
 
14
15
  const globals = {
@@ -16,16 +17,20 @@ const globals = {
16
17
  };
17
18
 
18
19
  class CommandError extends Error {
19
- constructor(message, { code, out, showOutput }) {
20
+ constructor(message, { code, out, userCommand }) {
20
21
  super(message);
21
22
  this.code = code;
22
23
  this.out = out;
23
- this.showOutput = showOutput;
24
+ this.userCommand = userCommand;
24
25
  }
25
26
  }
26
27
 
27
- // A simple Promise wrapper for child_process.spawn
28
- function spawnProcess(cmd, params = [], showOutput) {
28
+ // A simple Promise wrapper for child_process.spawn. If interactive is true,
29
+ // stdout becomes a tty when available and we cannot read the stdout from the
30
+ // command anymore. If interactive is "full", the error stream is also piped to
31
+ // the main process as TTY, so it is not readable anymore as well. For the
32
+ // commands using stderr as TTY it should be set to "full"
33
+ function spawnProcess(cmd, params = [], userCommand, interactive = false) {
29
34
  return new Promise((resolve, reject) => {
30
35
  let stdout = [];
31
36
  let stderr = [];
@@ -34,21 +39,41 @@ function spawnProcess(cmd, params = [], showOutput) {
34
39
  log(chalk.grey(`Spawning: ${cmd} ${params.join(' ')}`), true);
35
40
  }
36
41
 
37
- const command = spawn(cmd, params);
38
-
39
- command.stdout.on('data', function (data) {
40
- if (showOutput || globals.verbose) {
41
- process.stdout.write(data);
42
- }
43
- stdout.push(Buffer.from(data));
42
+ const isTTY =
43
+ process.stdin.isTTY && process.stdout.isTTY && process.stderr.isTTY;
44
+ const enableTTY = isTTY && !!interactive;
45
+ const enableErrorTTY = isTTY && interactive === 'full';
46
+
47
+ const command = spawn(cmd, params, {
48
+ // We should redirect streams together for the TTY to work
49
+ // properly if they are all used as TTY
50
+ stdio: [
51
+ enableTTY ? 'inherit' : 'pipe',
52
+ enableTTY ? 'inherit' : 'pipe',
53
+ enableErrorTTY ? 'inherit' : 'pipe',
54
+ ],
44
55
  });
45
56
 
46
- command.stderr.on('data', function (data) {
47
- if (showOutput || globals.verbose) {
48
- process.stderr.write(data);
49
- }
50
- stderr.push(Buffer.from(data));
51
- });
57
+ if (!enableTTY && (globals.verbose || userCommand)) {
58
+ command.stdout.pipe(process.stdout);
59
+ }
60
+
61
+ if (!enableErrorTTY && (globals.verbose || userCommand)) {
62
+ command.stderr.pipe(process.stderr);
63
+ }
64
+
65
+ // We shouldn't need to collect the data if it is a user command.
66
+ if (!enableTTY && !userCommand) {
67
+ command.stdout.on('data', function (data) {
68
+ stdout.push(Buffer.from(data));
69
+ });
70
+ }
71
+
72
+ if (!enableErrorTTY) {
73
+ command.stderr.on('data', function (data) {
74
+ stderr.push(Buffer.from(data));
75
+ });
76
+ }
52
77
 
53
78
  const errorHandler = (err) => {
54
79
  command.off('close', closeHandler);
@@ -65,7 +90,7 @@ function spawnProcess(cmd, params = [], showOutput) {
65
90
  {
66
91
  code,
67
92
  out: Buffer.concat(stderr).toString(),
68
- showOutput,
93
+ userCommand,
69
94
  }
70
95
  );
71
96
  reject(err);
@@ -77,29 +102,49 @@ function spawnProcess(cmd, params = [], showOutput) {
77
102
  });
78
103
  }
79
104
 
80
- function dockerExec(libdragonInfo, dockerParams, cmdWithParams, showOutput) {
105
+ function dockerExec(
106
+ libdragonInfo,
107
+ dockerParams,
108
+ cmdWithParams,
109
+ userCommand,
110
+ interactive
111
+ ) {
81
112
  // TODO: assert for invalid args
82
113
  const haveDockerParams =
83
114
  Array.isArray(dockerParams) && Array.isArray(cmdWithParams);
115
+ const isTTY = process.stdin.isTTY && process.stdout.isTTY;
116
+ // interactive and TTY?
117
+ const additionalParams =
118
+ isTTY && (haveDockerParams ? interactive : userCommand) ? ['-it'] : [];
84
119
  return spawnProcess(
85
120
  'docker',
86
121
  [
87
122
  'exec',
88
- ...(haveDockerParams ? dockerParams : []),
123
+ ...(haveDockerParams
124
+ ? [...dockerParams, ...additionalParams]
125
+ : additionalParams),
89
126
  libdragonInfo.containerId,
90
127
  ...(haveDockerParams ? cmdWithParams : dockerParams),
91
128
  ],
92
- haveDockerParams ? showOutput : cmdWithParams
129
+ haveDockerParams ? userCommand : cmdWithParams,
130
+ haveDockerParams ? interactive : userCommand
93
131
  );
94
132
  }
95
133
 
96
134
  /**
97
135
  * Invokes host git with provided params. If host does not have git, falls back
98
- * to the docker git, with the user set to the user running libdragon.
136
+ * to the docker git, with the nix user set to the user running libdragon.
99
137
  */
100
- async function runGitMaybeHost(libdragonInfo, params, showOutput) {
138
+ async function runGitMaybeHost(libdragonInfo, params, interactive = 'full') {
101
139
  try {
102
- return await spawnProcess('git', params, showOutput);
140
+ return await spawnProcess(
141
+ 'git',
142
+ params,
143
+ false,
144
+ // Windows git is breaking the TTY somehow - disable interactive for now
145
+ // We are not able to display progress for the initial clone b/c of this
146
+ /^win/.test(process.platform) ? false : interactive
147
+ );
103
148
  } catch (e) {
104
149
  if (!(e instanceof CommandError)) {
105
150
  return await dockerExec(
@@ -107,7 +152,8 @@ async function runGitMaybeHost(libdragonInfo, params, showOutput) {
107
152
  // Use the host user when initializing git as we will need access
108
153
  [...dockerHostUserParams(libdragonInfo)],
109
154
  ['git', ...params],
110
- showOutput
155
+ false,
156
+ interactive
111
157
  );
112
158
  }
113
159
  throw e;
@@ -148,16 +194,16 @@ async function findNPMRoot() {
148
194
  }
149
195
  }
150
196
 
151
- function dockerRelativeWorkdirParams(libdragonInfo) {
152
- return [
153
- '--workdir',
154
- '/libdragon/' +
155
- toPosixPath(path.relative(libdragonInfo.root, process.cwd())),
156
- ];
197
+ function dockerRelativeWorkdir(libdragonInfo) {
198
+ return (
199
+ CONTAINER_TARGET_PATH +
200
+ '/' +
201
+ toPosixPath(path.relative(libdragonInfo.root, process.cwd()))
202
+ );
157
203
  }
158
204
 
159
- function dockerByteSwapParams(libdragonInfo) {
160
- return libdragonInfo.options.BYTE_SWAP ? ['-e', 'N64_BYTE_SWAP=true'] : [];
205
+ function dockerRelativeWorkdirParams(libdragonInfo) {
206
+ return ['--workdir', dockerRelativeWorkdir(libdragonInfo)];
161
207
  }
162
208
 
163
209
  function dockerHostUserParams(libdragonInfo) {
@@ -174,6 +220,40 @@ async function findGitRoot() {
174
220
  }
175
221
  }
176
222
 
223
+ async function findContainerId(libdragonInfo) {
224
+ const idFile = path.join(libdragonInfo.root, '.git', CACHED_CONTAINER_FILE);
225
+ if (fs.existsSync(idFile)) {
226
+ const id = fs.readFileSync(idFile, { encoding: 'utf8' }).trim();
227
+ log(`Read containerId: ${id}`, true);
228
+ return id;
229
+ }
230
+
231
+ const candidates = (
232
+ await spawnProcess('docker', [
233
+ 'container',
234
+ 'ls',
235
+ '-a',
236
+ '--format',
237
+ '{{.}}{{.ID}}',
238
+ '-f',
239
+ 'volume=' + CONTAINER_TARGET_PATH,
240
+ ])
241
+ )
242
+ .split('\n')
243
+ .filter((s) => s.includes(`${libdragonInfo.root} `));
244
+
245
+ if (candidates.length > 0) {
246
+ const str = candidates[0];
247
+ const shortId = str.slice(-12);
248
+ const idIndex = str.indexOf(shortId);
249
+ const longId = str.slice(idIndex, idIndex + 64);
250
+ if (longId.length === 64) {
251
+ tryCacheContainerId({ ...libdragonInfo, containerId: longId });
252
+ return longId;
253
+ }
254
+ }
255
+ }
256
+
177
257
  async function checkContainerAndClean(libdragonInfo) {
178
258
  const id =
179
259
  libdragonInfo.containerId &&
@@ -224,62 +304,59 @@ async function readProjectInfo() {
224
304
  if (!info.root) {
225
305
  log('Could not find project root, set as cwd.', true);
226
306
  info.root = process.cwd();
227
- return info;
228
307
  }
229
308
 
230
309
  log(`Project root: ${info.root}`, true);
231
310
 
232
- const idFile = path.join(info.root, '.git', CACHED_CONTAINER_FILE);
233
- if (fs.existsSync(idFile)) {
234
- const id = fs.readFileSync(idFile, { encoding: 'utf8' }).trim();
235
- log(`Read containerId: ${id}`, true);
236
- info.containerId = id;
237
- }
238
-
239
311
  const imageFile = path.join(
240
312
  info.root,
241
313
  LIBDRAGON_PROJECT_MANIFEST,
242
314
  IMAGE_FILE
243
315
  );
244
316
 
317
+ // flag has the highest priority followed by the one read from the file
318
+ // and then if there is any matching container, name is read from it. As last
319
+ // option fallback to default value.
245
320
  if (fs.existsSync(imageFile) && !fs.statSync(imageFile).isDirectory()) {
246
321
  info.imageName = fs.readFileSync(imageFile, { encoding: 'utf8' }).trim();
247
322
  }
248
323
 
249
- return info;
250
- }
251
-
252
- async function updateImageName(libdragonInfo) {
253
- const manifestPath = path.join(
254
- libdragonInfo.root,
255
- LIBDRAGON_PROJECT_MANIFEST
256
- );
257
-
258
- // flag has the highest priority followed by the one read from the file
259
- // and then if there is any matching container, name is read from it. As last
260
- // option fallback to default value.
261
- let imageName = libdragonInfo.options.DOCKER_IMAGE ?? libdragonInfo.imageName;
324
+ info.containerId = await findContainerId(info);
325
+ log(`Active container id: ${info.containerId}`, true);
262
326
 
263
- if (!imageName) {
264
- // If still have the container, read the image name
265
- const containerId = await checkContainerAndClean(libdragonInfo);
266
- if (containerId) {
267
- imageName = await spawnProcess('docker', [
327
+ // If still have the container, read the image name from it
328
+ if (!info.imageName && (await checkContainerAndClean(info))) {
329
+ info.imageName = (
330
+ await spawnProcess('docker', [
268
331
  'container',
269
332
  'inspect',
270
- containerId,
333
+ info.containerId,
271
334
  '--format',
272
335
  '{{.Config.Image}}',
273
- ]);
274
- }
336
+ ])
337
+ ).trim();
338
+
339
+ // Cache the image name
340
+ await updateImageName(info);
275
341
  }
276
342
 
277
- imageName = imageName ?? DOCKER_HUB_IMAGE;
343
+ info.imageName = info.imageName ?? DOCKER_HUB_IMAGE;
344
+ log(`Active image name: ${info.imageName}`, true);
345
+ return info;
346
+ }
278
347
 
348
+ async function updateImageName(libdragonInfo) {
349
+ if (!libdragonInfo.imageName) return;
350
+ const manifestPath = path.join(
351
+ libdragonInfo.root,
352
+ LIBDRAGON_PROJECT_MANIFEST
353
+ );
279
354
  await createManifestIfNotExist(libdragonInfo);
280
- fs.writeFileSync(path.join(manifestPath, IMAGE_FILE), imageName);
281
- log(`Image name updated: ${imageName}`, true);
282
- return imageName;
355
+ fs.writeFileSync(
356
+ path.join(manifestPath, IMAGE_FILE),
357
+ libdragonInfo.imageName
358
+ );
359
+ log(`Image name updated: ${libdragonInfo.imageName}`, true);
283
360
  }
284
361
 
285
362
  /**
@@ -302,9 +379,17 @@ async function createManifestIfNotExist(libdragonInfo) {
302
379
  `Creating libdragon project configuration at \`${libdragonInfo.root}\`.`
303
380
  );
304
381
  fs.mkdirSync(manifestPath);
305
- return true;
306
382
  }
307
- return false;
383
+ }
384
+
385
+ function tryCacheContainerId(libdragonInfo) {
386
+ const gitFolder = path.join(libdragonInfo.root, '.git');
387
+ if (fs.existsSync(gitFolder) && fs.statSync(gitFolder).isDirectory()) {
388
+ fs.writeFileSync(
389
+ path.join(libdragonInfo.root, '.git', CACHED_CONTAINER_FILE),
390
+ libdragonInfo.containerId
391
+ );
392
+ }
308
393
  }
309
394
 
310
395
  function toPosixPath(p) {
@@ -337,9 +422,10 @@ module.exports = {
337
422
  log,
338
423
  dockerExec,
339
424
  dockerRelativeWorkdirParams,
340
- dockerByteSwapParams,
341
425
  runGitMaybeHost,
342
426
  dockerHostUserParams,
427
+ dockerRelativeWorkdir,
428
+ tryCacheContainerId,
343
429
  CommandError,
344
430
  globals,
345
431
  };
@@ -0,0 +1,129 @@
1
+ const chalk = require('chalk');
2
+ const commandLineUsage = require('command-line-usage');
3
+
4
+ const { log } = require('./helpers');
5
+
6
+ const printUsage = (_, actionArr) => {
7
+ const globalOptionDefinitions = [
8
+ {
9
+ name: 'verbose',
10
+ description: 'Be verbose',
11
+ alias: 'v',
12
+ group: 'global',
13
+ },
14
+ ];
15
+
16
+ const optionDefinitions = [
17
+ {
18
+ name: 'image',
19
+ description: 'Provide a custom image.',
20
+ alias: 'i',
21
+ typeLabel: '<docker-image>',
22
+ group: 'docker',
23
+ },
24
+ ];
25
+
26
+ const actions = {
27
+ help: {
28
+ name: 'help [action]',
29
+ summary: 'Display this help information or details for the given action.',
30
+ },
31
+ init: {
32
+ name: 'init',
33
+ summary: 'Create a libdragon project in the current directory.',
34
+ 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.
35
+
36
+ A git repository and a submodule at \`./libdragon\` will also be created. Do not remove the \`.libdragon\` folder and commit its contents if you are using git, as it keeps persistent libdragon project information.
37
+
38
+ 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.`,
39
+ group: ['docker'],
40
+ },
41
+ make: {
42
+ name: 'make [params]',
43
+ summary: 'Run the libdragon build system in the current directory.',
44
+ description: `Runs the libdragon build system in the current directory. It will mirror your current working directory to the container.
45
+
46
+ This action is a shortcut to the \`exec\` action under the hood.`,
47
+ },
48
+ exec: {
49
+ name: 'exec <command>',
50
+ summary: 'Execute given command in the current directory.',
51
+ 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.
52
+
53
+ 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.
54
+
55
+ 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.`,
56
+ },
57
+ start: {
58
+ name: 'start',
59
+ summary: 'Start the container for current project.',
60
+ description:
61
+ '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.',
62
+ },
63
+ name: {
64
+ name: 'stop',
65
+ summary: 'Stop the container for current project.',
66
+ description:
67
+ 'Stop the container assigned to the current libdragon project.',
68
+ },
69
+ install: {
70
+ name: 'install',
71
+ summary: 'Vendor libdragon as is.',
72
+ group: ['docker'],
73
+ 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 \`libdragon\` at the root of the project. If you are not working on libdragon, you can just use the \`update\` action instead.
74
+
75
+ This can be useful to recover from a half-baked container.`,
76
+ },
77
+ update: {
78
+ name: 'update',
79
+ summary: 'Update libdragon and do an install.',
80
+ description:
81
+ 'This action will update the submodule from the remote branch (`trunk`) with a merge strategy and then perform a `libdragon install`. You can use the `install` action to only update all libdragon related artifacts in the container.',
82
+ group: ['docker'],
83
+ },
84
+ };
85
+
86
+ const actionsToShow = actionArr
87
+ ?.filter((action) => Object.keys(actions).includes(action))
88
+ .filter((action) => !['help'].includes(action));
89
+
90
+ const sections = [
91
+ {
92
+ header: chalk.green('Usage:'),
93
+ content: 'libdragon [flags] <action>',
94
+ },
95
+ ...(actionsToShow?.length
96
+ ? actionsToShow.flatMap((action) => [
97
+ {
98
+ header: chalk.green(`${action} action:`),
99
+ content: actions[action].description,
100
+ },
101
+ actions[action].group
102
+ ? {
103
+ header: `accepted flags:`,
104
+ optionList: optionDefinitions,
105
+ group: actions[action].group,
106
+ }
107
+ : {},
108
+ ])
109
+ : [
110
+ {
111
+ header: chalk.green('Available Commands:'),
112
+ content: Object.values(actions).map((action) => ({
113
+ name: action.name,
114
+ summary: action.summary,
115
+ })),
116
+ },
117
+ ]),
118
+ {
119
+ header: chalk.green('Global flags:'),
120
+ optionList: globalOptionDefinitions,
121
+ },
122
+ ];
123
+ const usage = commandLineUsage(sections);
124
+ log(usage);
125
+ };
126
+
127
+ module.exports = {
128
+ printUsage,
129
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libdragon",
3
- "version": "10.0.0",
3
+ "version": "10.3.1",
4
4
  "description": "This is a docker wrapper for libdragon",
5
5
  "main": "index.js",
6
6
  "engines": {
@@ -24,7 +24,8 @@
24
24
  "url": "git+https://github.com/anacierdem/libdragon-docker.git"
25
25
  },
26
26
  "files": [
27
- "modules/**/*"
27
+ "modules/**/*",
28
+ "skeleton/**"
28
29
  ],
29
30
  "author": "Ali Naci Erdem",
30
31
  "license": "MIT",
@@ -34,10 +35,11 @@
34
35
  "homepage": "https://github.com/anacierdem/libdragon-docker#readme",
35
36
  "dependencies": {
36
37
  "chalk": "^4.1.0",
38
+ "command-line-usage": "^6.1.1",
37
39
  "lodash": "^4.17.20"
38
40
  },
39
41
  "devDependencies": {
40
- "ed64": "^2.0.2",
42
+ "ed64": "^2.0.3",
41
43
  "eslint": "^7.32.0",
42
44
  "pkg": "^5.3.1",
43
45
  "prettier": "^2.4.0"
@@ -0,0 +1,18 @@
1
+ V=1
2
+ SOURCE_DIR=src
3
+ BUILD_DIR=build
4
+ include $(N64_INST)/include/n64.mk
5
+
6
+ src=main.c
7
+
8
+ all: hello.z64
9
+
10
+ hello.z64: N64_ROM_TITLE="Hello World"
11
+ $(BUILD_DIR)/hello.elf: $(src:%.c=$(BUILD_DIR)/%.o)
12
+
13
+ clean:
14
+ rm -f $(BUILD_DIR)/* hello.z64
15
+
16
+ -include $(wildcard $(BUILD_DIR)/*.d)
17
+
18
+ .PHONY: all clean
@@ -0,0 +1,15 @@
1
+ #include <stdio.h>
2
+
3
+ #include <libdragon.h>
4
+
5
+ int main(void)
6
+ {
7
+ console_init();
8
+
9
+ debug_init_usblog();
10
+ console_set_debug(true);
11
+
12
+ printf("Hello world!\n");
13
+
14
+ while(1) {}
15
+ }