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.
@@ -4,7 +4,7 @@ const path = require('path');
4
4
  const chalk = require('chalk');
5
5
 
6
6
  const { fn: install } = require('./install');
7
- const { fn: start } = require('./start');
7
+ const { start } = require('./start');
8
8
  const {
9
9
  installDependencies,
10
10
  tryCacheContainerId,
@@ -23,8 +23,10 @@ const {
23
23
  copyDirContents,
24
24
  CommandError,
25
25
  ParameterError,
26
+ ValidationError,
27
+ toPosixPath,
28
+ toNativePath,
26
29
  } = require('../helpers');
27
- const { setProjectInfoToSave } = require('../project-info');
28
30
 
29
31
  const autoVendor = async (libdragonInfo) => {
30
32
  await runGitMaybeHost(libdragonInfo, ['init']);
@@ -81,55 +83,60 @@ const autoVendor = async (libdragonInfo) => {
81
83
  async function init(libdragonInfo) {
82
84
  log(`Initializing a libdragon project at ${libdragonInfo.root}`);
83
85
 
84
- // TODO: use exists instead & check if it is a directory
85
- const files = await fs.readdir(libdragonInfo.root);
86
+ let newInfo = libdragonInfo;
86
87
 
87
- const manifestFile = files.find(
88
- (name) => name === LIBDRAGON_PROJECT_MANIFEST
89
- );
88
+ // Validate manifest
89
+ const manifestPath = path.join(newInfo.root, LIBDRAGON_PROJECT_MANIFEST);
90
+ const manifestStats = await fs.stat(manifestPath).catch((e) => {
91
+ if (e.code !== 'ENOENT') throw e;
92
+ return false;
93
+ });
90
94
 
91
- let newInfo = libdragonInfo;
95
+ if (manifestStats && !manifestStats.isDirectory()) {
96
+ throw new ValidationError(
97
+ 'There is already a `.libdragon` file and it is not a directory.'
98
+ );
99
+ }
92
100
 
93
101
  // Validate vendoring strategy. Do not allow a switch after successful initialization
94
102
  if (
95
- manifestFile &&
103
+ newInfo.haveProjectConfig &&
96
104
  newInfo.options.VENDOR_STRAT &&
97
105
  newInfo.options.VENDOR_STRAT !== 'manual' &&
98
106
  newInfo.vendorStrategy !== newInfo.options.VENDOR_STRAT
99
107
  ) {
100
- throw new Error(
108
+ throw new ParameterError(
101
109
  `Requested strategy switch: ${newInfo.vendorStrategy} -> ${newInfo.options.VENDOR_STRAT} It is not possible to switch vendoring strategy after initializing a project. You can always switch to manual and handle libdragon yourself.`
102
110
  );
103
111
  }
104
112
 
105
113
  // Update the strategy information for the project if the flag is provided
106
114
  if (newInfo.options.VENDOR_STRAT) {
107
- newInfo = setProjectInfoToSave({
108
- ...newInfo,
109
- vendorStrategy: newInfo.options.VENDOR_STRAT,
110
- });
115
+ newInfo.vendorStrategy = newInfo.options.VENDOR_STRAT;
111
116
  }
112
117
 
113
118
  // Update the directory information for the project if the flag is provided
114
119
  if (newInfo.options.VENDOR_DIR) {
120
+ const relativeVendorDir = path.relative(
121
+ libdragonInfo.root,
122
+ libdragonInfo.options.VENDOR_DIR
123
+ );
115
124
  // Validate vendoring path
116
- if (path.isAbsolute(newInfo.options.VENDOR_DIR)) {
125
+ if (relativeVendorDir.startsWith('..')) {
117
126
  throw new ParameterError(
118
- '`--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.`
119
128
  );
120
129
  }
121
130
 
122
- newInfo = setProjectInfoToSave({
123
- ...newInfo,
124
- vendorDirectory: newInfo.options.VENDOR_DIR,
125
- });
131
+ // Immeditately convert it to a posix and relative path
132
+ newInfo.vendorDirectory = toPosixPath(relativeVendorDir);
126
133
  }
127
134
 
128
- if (manifestFile) {
135
+ if (newInfo.haveProjectConfig) {
129
136
  log(
130
137
  `${path.join(
131
138
  newInfo.root,
132
- manifestFile
139
+ LIBDRAGON_PROJECT_MANIFEST
133
140
  )} exists. This is already a libdragon project, starting it...`
134
141
  );
135
142
  if (newInfo.options.DOCKER_IMAGE) {
@@ -137,31 +144,36 @@ async function init(libdragonInfo) {
137
144
  `Not changing docker image. Use the install action if you want to override the image.`
138
145
  );
139
146
  }
140
- await install(newInfo);
141
- return;
147
+ // TODO: we may make sure git and submodule is initialized here
148
+ return await install(newInfo);
142
149
  }
143
150
 
144
- newInfo.imageName =
145
- (await updateImage(newInfo, newInfo.imageName)) || newInfo.imageName;
151
+ await updateImage(newInfo, newInfo.imageName);
152
+
146
153
  // Download image and start it
147
- const containerReadyPromise = start(newInfo).then((newId) => ({
154
+ const containerReadyPromise = start(newInfo, true).then((newId) => ({
148
155
  ...newInfo,
149
156
  containerId: newId,
150
157
  }));
151
158
 
152
159
  let vendorAndGitReadyPromise = containerReadyPromise;
153
160
  if (newInfo.vendorStrategy !== 'manual') {
154
- const libdragonFile = files.find((name) =>
155
- name.match(
156
- new RegExp(`^${path.relative(newInfo.root, newInfo.vendorDirectory)}$`)
157
- )
161
+ const vendorTarget = path.relative(
162
+ newInfo.root,
163
+ toNativePath(newInfo.vendorDirectory)
158
164
  );
165
+ const [vendorTargetExists] = await Promise.all([
166
+ fs.stat(vendorTarget).catch((e) => {
167
+ if (e.code !== 'ENOENT') throw e;
168
+ return false;
169
+ }),
170
+ containerReadyPromise,
171
+ ]);
159
172
 
160
- if (libdragonFile) {
161
- throw new Error(
162
- `${path.join(
163
- newInfo.root,
164
- libdragonFile
173
+ if (vendorTargetExists) {
174
+ throw new ValidationError(
175
+ `${path.resolve(
176
+ vendorTarget
165
177
  )} already exists. That is the libdragon vendoring target, please remove and retry.`
166
178
  );
167
179
  }
@@ -181,10 +193,21 @@ async function init(libdragonInfo) {
181
193
  ]);
182
194
 
183
195
  log(chalk.green(`libdragon ready at \`${newInfo.root}\`.`));
196
+ return newInfo;
184
197
  }
185
198
 
186
199
  module.exports = {
187
200
  name: 'init',
188
201
  fn: init,
189
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
+ },
190
213
  };
@@ -1,52 +1,56 @@
1
+ const chalk = require('chalk');
2
+
3
+ const { installDependencies, mustHaveProject } = require('./utils');
4
+ const { start } = require('./start');
5
+ const { updateAndStart } = require('./update-and-start');
1
6
  const { log } = require('../helpers');
2
- const {
3
- destroyContainer,
4
- installDependencies,
5
- updateImage,
6
- } = require('./utils');
7
- const { fn: start } = require('./start');
8
7
 
9
8
  /**
10
9
  * Updates the image if flag is provided and install vendors onto the container.
11
10
  * We should probably remove the image installation responsibility from this
12
- * action but it might be a breaking change. Maybe we can keep it backward
13
- * compatible with additional flags.
11
+ * action but it might be a breaking change.
14
12
  * @param libdragonInfo
13
+ * @param skipUpdate This is added to skip the update when calling this from
14
+ * the update action as it already does an update itself. install doing an image
15
+ * update is pretty much a useless operation, but let's keep it in case someone
16
+ * depends on it. It used to only update the image if the flag is provided and
17
+ * we still keep that logic but with a deprecation warning.
15
18
  */
16
- const install = async (libdragonInfo) => {
17
- let containerId;
18
- const oldImageName = libdragonInfo.imageName;
19
+ const install = async (libdragonInfo, skipUpdate) => {
20
+ await mustHaveProject(libdragonInfo);
21
+ let updatedInfo = libdragonInfo;
19
22
  const imageName = libdragonInfo.options.DOCKER_IMAGE;
20
- // If an image is provided, always attempt to install it
21
- // See https://github.com/anacierdem/libdragon-docker/issues/47
22
- if (imageName) {
23
- log(`Changing image from \`${oldImageName}\` to \`${imageName}\``);
24
-
25
- // Download the new image and if it is different, re-create the container
26
- if (await updateImage(libdragonInfo, imageName)) {
27
- await destroyContainer(libdragonInfo);
28
- }
29
-
30
- containerId = await start({
31
- ...libdragonInfo,
32
- imageName,
33
- });
23
+ // If an image is provided, attempt to install
24
+ if (imageName && skipUpdate !== true) {
25
+ log(
26
+ chalk.yellow(
27
+ 'Using `install` action to update the docker image is deprecated. Use the `update` action instead.'
28
+ )
29
+ );
30
+ updatedInfo = await updateAndStart(libdragonInfo);
34
31
  } else {
35
32
  // Make sure existing one is running
36
- containerId = await start(libdragonInfo);
33
+ updatedInfo = {
34
+ ...updatedInfo,
35
+ containerId: await start(libdragonInfo),
36
+ };
37
37
  }
38
38
 
39
39
  // Re-install vendors on new image
40
40
  // TODO: skip this if unnecessary
41
- await installDependencies({
42
- ...libdragonInfo,
43
- imageName,
44
- containerId,
45
- });
41
+ await installDependencies(updatedInfo);
42
+ return updatedInfo;
46
43
  };
47
44
 
48
45
  module.exports = {
49
46
  name: 'install',
50
47
  fn: install,
51
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
+ },
52
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,12 +2,12 @@ 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,
9
8
  checkContainerRunning,
10
9
  destroyContainer,
10
+ mustHaveProject,
11
11
  } = require('./utils');
12
12
 
13
13
  /**
@@ -75,19 +75,17 @@ const initContainer = async (libdragonInfo) => {
75
75
  chalk.green(`Successfully initialized docker container: ${name.trim()}`)
76
76
  );
77
77
 
78
- // Schedule an update to write image name
79
- setProjectInfoToSave(libdragonInfo);
80
78
  return newId;
81
79
  };
82
80
 
83
- const start = async (libdragonInfo) => {
81
+ const start = async (libdragonInfo, skipProjectCheck) => {
82
+ !skipProjectCheck && (await mustHaveProject(libdragonInfo));
84
83
  const running =
85
84
  libdragonInfo.containerId &&
86
85
  (await checkContainerRunning(libdragonInfo.containerId));
87
86
 
88
87
  if (running) {
89
88
  log(`Container ${running} already running.`, true);
90
- log(libdragonInfo.containerId);
91
89
  return running;
92
90
  }
93
91
 
@@ -96,19 +94,29 @@ const start = async (libdragonInfo) => {
96
94
  if (!id) {
97
95
  log(`Container does not exist, re-initializing...`, true);
98
96
  id = await initContainer(libdragonInfo);
99
- log(id);
100
97
  return id;
101
98
  }
102
99
 
103
100
  log(`Starting container: ${id}`, true);
104
101
  await spawnProcess('docker', ['container', 'start', id]);
105
102
 
106
- log(id);
107
103
  return id;
108
104
  };
109
105
 
110
106
  module.exports = {
111
107
  name: 'start',
112
- fn: start,
108
+ fn: async (libdragonInfo) => {
109
+ const containerId = await start(libdragonInfo);
110
+ log(containerId);
111
+ return { ...libdragonInfo, containerId };
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
  };
@@ -0,0 +1,29 @@
1
+ const { spawnProcess } = require('../helpers');
2
+
3
+ const { checkContainerRunning, mustHaveProject } = require('./utils');
4
+
5
+ const stop = async (libdragonInfo) => {
6
+ await mustHaveProject(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
+ return libdragonInfo;
16
+ };
17
+
18
+ module.exports = {
19
+ name: 'stop',
20
+ fn: stop,
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
+ },
29
+ };
@@ -0,0 +1,33 @@
1
+ const { log } = require('../helpers');
2
+ const { updateImage, destroyContainer } = require('./utils');
3
+ const { start } = require('./start');
4
+
5
+ async function updateAndStart(libdragonInfo) {
6
+ const oldImageName = libdragonInfo.imageName;
7
+ const imageName = libdragonInfo.options.DOCKER_IMAGE ?? oldImageName;
8
+ // If an image is provided, always attempt to install it
9
+ // See https://github.com/anacierdem/libdragon-docker/issues/47
10
+ if (oldImageName !== imageName) {
11
+ log(`Updating image from \`${oldImageName}\` to \`${imageName}\``);
12
+ } else {
13
+ log(`Updating image \`${oldImageName}\``);
14
+ }
15
+
16
+ // Download the new image and if it is different, re-create the container
17
+ if (await updateImage(libdragonInfo, imageName)) {
18
+ await destroyContainer(libdragonInfo);
19
+ }
20
+
21
+ return {
22
+ ...libdragonInfo,
23
+ imageName,
24
+ containerId: await start({
25
+ ...libdragonInfo,
26
+ imageName,
27
+ }),
28
+ };
29
+ }
30
+
31
+ module.exports = {
32
+ updateAndStart,
33
+ };
@@ -1,18 +1,12 @@
1
- const path = require('path');
2
-
3
1
  const { log } = require('../helpers');
4
2
  const { LIBDRAGON_GIT, LIBDRAGON_BRANCH } = require('../constants');
5
- const { runGitMaybeHost } = require('./utils');
6
- const { fn: start } = require('./start');
3
+ const { runGitMaybeHost, mustHaveProject } = require('./utils');
7
4
  const { fn: install } = require('./install');
5
+ const { updateAndStart } = require('./update-and-start');
8
6
 
9
7
  const update = async (libdragonInfo) => {
10
- const containerId = await start(libdragonInfo);
11
-
12
- const newInfo = {
13
- ...libdragonInfo,
14
- containerId,
15
- };
8
+ await mustHaveProject(libdragonInfo);
9
+ const newInfo = await updateAndStart(libdragonInfo);
16
10
 
17
11
  if (newInfo.vendorStrategy !== 'manual') {
18
12
  log(`Updating ${newInfo.vendorStrategy}...`);
@@ -31,18 +25,28 @@ const update = async (libdragonInfo) => {
31
25
  'subtree',
32
26
  'pull',
33
27
  '--prefix',
34
- path.relative(libdragonInfo.root, libdragonInfo.vendorDirectory),
28
+ libdragonInfo.vendorDirectory,
35
29
  LIBDRAGON_GIT,
36
30
  LIBDRAGON_BRANCH,
37
31
  '--squash',
38
32
  ]);
39
33
  }
40
34
 
41
- await install(newInfo);
35
+ // The second parameter forces it to skip the image update step as we already
36
+ // do that above.
37
+ return await install(newInfo, true);
42
38
  };
43
39
 
44
40
  module.exports = {
45
41
  name: 'update',
46
42
  fn: update,
47
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
+ },
48
52
  };