libdragon 10.4.1 → 10.4.2

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,35 @@
1
1
  # Change Log
2
2
 
3
+ ## [10.4.2] - 2022-04-03
4
+
5
+ ### Fixed
6
+
7
+ - Make sure actions depending on an `init` fail in a non-project directory to
8
+ keep the project state consistent. This fixes #51.
9
+ - `update` action now tries to update the toolchain image as well. Previously
10
+ this was not the case contrary to what someone would expect. Considering it won't
11
+ change the behaviour for non-latest images and the toolchain did not have any
12
+ breaking changes for a long time, this is not considered a breaking change either.
13
+ - `start` action was printing stuff other than the container id. It doesn't
14
+ anymore.
15
+ - Stop unnecessarily printing container id and a few messages related to updates.
16
+ - Fix a potential race condition that might cause unexpected failures.
17
+ - Correct some errors' exit codes.
18
+ ### Added
19
+
20
+ - A new exit code (`4`) to represent unexpected conditions.
21
+
22
+ ### Changed
23
+
24
+ - Deprecated providing the image flag for `install` action by displaying a
25
+ warning and removing it from documentation, without changing behaviour even
26
+ though it is higly unlikely this feature was ever used. It mainly exists for
27
+ historical reasons and it wil be removed in next major release.
28
+ - Update documentation to warn against changing strategy is a one way operation.
29
+ - Update documentation to reflect `update` action changes.
30
+ - Minor refactors.
31
+ - Update submodule for local environment.
32
+
3
33
  ## [10.4.1] - 2022-03-23
4
34
 
5
35
  ### Fixed
package/index.js CHANGED
@@ -8,9 +8,14 @@ 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
+ } = require('./modules/helpers');
14
19
  const { readProjectInfo, writeProjectInfo } = require('./modules/project-info');
15
20
 
16
21
  let options = {},
@@ -127,6 +132,12 @@ readProjectInfo()
127
132
  printUsage(undefined, [currentAction.name]);
128
133
  process.exit(STATUS_BAD_PARAM);
129
134
  }
135
+
136
+ if (e instanceof ValidationError) {
137
+ console.error(chalk.red(e.message));
138
+ process.exit(STATUS_VALIDATION_ERROR);
139
+ }
140
+
130
141
  const userTargetedError = e instanceof CommandError && e.userCommand;
131
142
 
132
143
  // Show additional information to user if verbose or we did a mistake
@@ -3,8 +3,12 @@ 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 {
8
+ dockerHostUserParams,
9
+ installDependencies,
10
+ mustHaveProject,
11
+ } = require('./utils');
8
12
 
9
13
  function dockerRelativeWorkdir(libdragonInfo) {
10
14
  return (
@@ -19,6 +23,8 @@ function dockerRelativeWorkdirParams(libdragonInfo) {
19
23
  }
20
24
 
21
25
  const exec = async (libdragonInfo, commandAndParams) => {
26
+ await mustHaveProject(libdragonInfo);
27
+
22
28
  log(
23
29
  `Running ${commandAndParams[0]} at ${dockerRelativeWorkdir(
24
30
  libdragonInfo
@@ -39,15 +39,17 @@ 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
+ // TODO: move these to respective actions
52
+
51
53
  const actions = {
52
54
  help: {
53
55
  name: 'help [action]',
@@ -68,7 +70,7 @@ const printUsage = (_, actionArr) => {
68
70
  summary: 'Run the libdragon build system in the current directory.',
69
71
  description: `Runs the libdragon build system in the current directory. It will mirror your current working directory to the container.
70
72
 
71
- This action is a shortcut to the \`exec\` action under the hood.`,
73
+ Must be run in an initialized libdragon project. This action is a shortcut to the \`exec\` action under the hood.`,
72
74
  },
73
75
  exec: {
74
76
  name: 'exec <command>',
@@ -77,33 +79,37 @@ const printUsage = (_, actionArr) => {
77
79
 
78
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.
79
81
 
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.`,
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.`,
81
85
  },
82
86
  start: {
83
87
  name: 'start',
84
88
  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.',
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.`,
87
92
  },
88
93
  name: {
89
94
  name: 'stop',
90
95
  summary: 'Stop the container for current project.',
91
- description:
92
- 'Stop the container assigned to the current libdragon project.',
96
+ description: `Stop the container assigned to the current libdragon project.
97
+
98
+ Must be run in an initialized libdragon project.`,
93
99
  },
94
100
  install: {
95
101
  name: 'install',
96
102
  summary: 'Vendor libdragon as is.',
97
- group: ['docker'],
98
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.
99
104
 
100
- This can be useful to recover from a half-baked container.`,
105
+ Must be run in an initialized libdragon project. This can be useful to recover from a half-baked container.`,
101
106
  },
102
107
  update: {
103
108
  name: 'update',
104
109
  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.',
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.`,
107
113
  group: ['docker'],
108
114
  },
109
115
  };
@@ -1,19 +1,5 @@
1
- const { spawnProcess } = require('../helpers');
2
- const { checkContainerRunning } = require('./utils');
3
-
4
1
  const { fn: exec } = require('./exec');
5
2
 
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
3
  const make = async (libdragonInfo, params) => {
18
4
  await exec(libdragonInfo, ['make', ...params]);
19
5
  };
@@ -21,11 +7,7 @@ const make = async (libdragonInfo, params) => {
21
7
  // TODO: separate into files
22
8
  module.exports = {
23
9
  start: require('./start'),
24
- stop: {
25
- name: 'stop',
26
- fn: stop,
27
- showStatus: false, // This will only print out the id
28
- },
10
+ stop: require('./stop'),
29
11
  init: require('./init'),
30
12
 
31
13
  exec: require('./exec'),
@@ -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,6 +23,7 @@ const {
23
23
  copyDirContents,
24
24
  CommandError,
25
25
  ParameterError,
26
+ ValidationError,
26
27
  } = require('../helpers');
27
28
  const { setProjectInfoToSave } = require('../project-info');
28
29
 
@@ -81,23 +82,29 @@ const autoVendor = async (libdragonInfo) => {
81
82
  async function init(libdragonInfo) {
82
83
  log(`Initializing a libdragon project at ${libdragonInfo.root}`);
83
84
 
84
- // TODO: use exists instead & check if it is a directory
85
- const files = await fs.readdir(libdragonInfo.root);
85
+ let newInfo = libdragonInfo;
86
86
 
87
- const manifestFile = files.find(
88
- (name) => name === LIBDRAGON_PROJECT_MANIFEST
89
- );
87
+ // Validate manifest
88
+ const manifestPath = path.join(newInfo.root, LIBDRAGON_PROJECT_MANIFEST);
89
+ const manifestStats = await fs.stat(manifestPath).catch((e) => {
90
+ if (e.code !== 'ENOENT') throw e;
91
+ return false;
92
+ });
90
93
 
91
- let newInfo = libdragonInfo;
94
+ if (manifestStats && !manifestStats.isDirectory()) {
95
+ throw new ValidationError(
96
+ 'There is already a `.libdragon` file and it is not a directory.'
97
+ );
98
+ }
92
99
 
93
100
  // Validate vendoring strategy. Do not allow a switch after successful initialization
94
101
  if (
95
- manifestFile &&
102
+ newInfo.haveProjectConfig &&
96
103
  newInfo.options.VENDOR_STRAT &&
97
104
  newInfo.options.VENDOR_STRAT !== 'manual' &&
98
105
  newInfo.vendorStrategy !== newInfo.options.VENDOR_STRAT
99
106
  ) {
100
- throw new Error(
107
+ throw new ParameterError(
101
108
  `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
109
  );
103
110
  }
@@ -125,11 +132,11 @@ async function init(libdragonInfo) {
125
132
  });
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,6 +144,7 @@ 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
  }
147
+ // TODO: we may make sure git and submodule is initialized here
140
148
  await install(newInfo);
141
149
  return;
142
150
  }
@@ -144,24 +152,27 @@ async function init(libdragonInfo) {
144
152
  newInfo.imageName =
145
153
  (await updateImage(newInfo, newInfo.imageName)) || newInfo.imageName;
146
154
  // Download image and start it
147
- const containerReadyPromise = start(newInfo).then((newId) => ({
155
+ const containerReadyPromise = start(newInfo, true).then((newId) => ({
148
156
  ...newInfo,
149
157
  containerId: newId,
150
158
  }));
151
159
 
152
160
  let vendorAndGitReadyPromise = containerReadyPromise;
153
161
  if (newInfo.vendorStrategy !== 'manual') {
154
- const libdragonFile = files.find((name) =>
155
- name.match(
156
- new RegExp(`^${path.relative(newInfo.root, newInfo.vendorDirectory)}$`)
157
- )
158
- );
162
+ const vendorTarget = path.relative(newInfo.root, newInfo.vendorDirectory);
163
+ const [vendorTargetExists] = await Promise.all([
164
+ fs.stat(vendorTarget).catch((e) => {
165
+ if (e.code !== 'ENOENT') throw e;
166
+ return false;
167
+ }),
168
+ containerReadyPromise,
169
+ ]);
159
170
 
160
- if (libdragonFile) {
161
- throw new Error(
162
- `${path.join(
171
+ if (vendorTargetExists) {
172
+ throw new ValidationError(
173
+ `${path.resolve(
163
174
  newInfo.root,
164
- libdragonFile
175
+ newInfo.vendorDirectory
165
176
  )} already exists. That is the libdragon vendoring target, please remove and retry.`
166
177
  );
167
178
  }
@@ -1,48 +1,44 @@
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);
46
42
  };
47
43
 
48
44
  module.exports = {
@@ -8,6 +8,7 @@ const {
8
8
  checkContainerAndClean,
9
9
  checkContainerRunning,
10
10
  destroyContainer,
11
+ mustHaveProject,
11
12
  } = require('./utils');
12
13
 
13
14
  /**
@@ -80,14 +81,14 @@ const initContainer = async (libdragonInfo) => {
80
81
  return newId;
81
82
  };
82
83
 
83
- const start = async (libdragonInfo) => {
84
+ const start = async (libdragonInfo, skipProjectCheck) => {
85
+ !skipProjectCheck && (await mustHaveProject(libdragonInfo));
84
86
  const running =
85
87
  libdragonInfo.containerId &&
86
88
  (await checkContainerRunning(libdragonInfo.containerId));
87
89
 
88
90
  if (running) {
89
91
  log(`Container ${running} already running.`, true);
90
- log(libdragonInfo.containerId);
91
92
  return running;
92
93
  }
93
94
 
@@ -96,19 +97,18 @@ const start = async (libdragonInfo) => {
96
97
  if (!id) {
97
98
  log(`Container does not exist, re-initializing...`, true);
98
99
  id = await initContainer(libdragonInfo);
99
- log(id);
100
100
  return id;
101
101
  }
102
102
 
103
103
  log(`Starting container: ${id}`, true);
104
104
  await spawnProcess('docker', ['container', 'start', id]);
105
105
 
106
- log(id);
107
106
  return id;
108
107
  };
109
108
 
110
109
  module.exports = {
111
110
  name: 'start',
112
- fn: start,
111
+ fn: async (libdragonInfo) => log(await start(libdragonInfo)),
112
+ start,
113
113
  showStatus: false, // This will only print out the id
114
114
  };
@@ -0,0 +1,21 @@
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
+ };
16
+
17
+ module.exports = {
18
+ name: 'stop',
19
+ fn: stop,
20
+ showStatus: false, // This will only print out the id
21
+ };
@@ -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
+ };
@@ -2,17 +2,13 @@ const path = require('path');
2
2
 
3
3
  const { log } = require('../helpers');
4
4
  const { LIBDRAGON_GIT, LIBDRAGON_BRANCH } = require('../constants');
5
- const { runGitMaybeHost } = require('./utils');
6
- const { fn: start } = require('./start');
5
+ const { runGitMaybeHost, mustHaveProject } = require('./utils');
7
6
  const { fn: install } = require('./install');
7
+ const { updateAndStart } = require('./update-and-start');
8
8
 
9
9
  const update = async (libdragonInfo) => {
10
- const containerId = await start(libdragonInfo);
11
-
12
- const newInfo = {
13
- ...libdragonInfo,
14
- containerId,
15
- };
10
+ await mustHaveProject(libdragonInfo);
11
+ const newInfo = await updateAndStart(libdragonInfo);
16
12
 
17
13
  if (newInfo.vendorStrategy !== 'manual') {
18
14
  log(`Updating ${newInfo.vendorStrategy}...`);
@@ -38,7 +34,9 @@ const update = async (libdragonInfo) => {
38
34
  ]);
39
35
  }
40
36
 
41
- await install(newInfo);
37
+ // The second parameter forces it to skip the image update step as we already
38
+ // do that above.
39
+ await install(newInfo, true);
42
40
  };
43
41
 
44
42
  module.exports = {
@@ -18,6 +18,8 @@ const {
18
18
  dirExists,
19
19
  assert,
20
20
  CommandError,
21
+ ValidationError,
22
+ ParameterError,
21
23
  } = require('../helpers');
22
24
 
23
25
  function dockerHostUserParams(libdragonInfo) {
@@ -45,7 +47,7 @@ const installDependencies = async (libdragonInfo) => {
45
47
  'build.sh'
46
48
  );
47
49
  if (!(await fileExists(buildScriptPath))) {
48
- throw new Error(
50
+ throw new ValidationError(
49
51
  `build.sh not found. Make sure you have a vendored libdragon copy at ${libdragonInfo.vendorDirectory}`
50
52
  );
51
53
  }
@@ -103,7 +105,7 @@ const installDependencies = async (libdragonInfo) => {
103
105
 
104
106
  if (paths.length > 1) {
105
107
  reject(
106
- new Error(
108
+ new ValidationError(
107
109
  `Using same dependency with different versions is not supported! ${name}`
108
110
  )
109
111
  );
@@ -173,7 +175,7 @@ const updateImage = async (libdragonInfo, newImageName) => {
173
175
  'docker',
174
176
  ['images', '-q', '--no-trunc', newImageName],
175
177
  false,
176
- libdragonInfo.showStatus
178
+ false
177
179
  );
178
180
 
179
181
  // Attempt to compare digests if the new image name is the same
@@ -186,14 +188,15 @@ const updateImage = async (libdragonInfo, newImageName) => {
186
188
  const newDigest = await getDigest();
187
189
 
188
190
  if (existingDigest === newDigest) {
189
- libdragonInfo.showStatus && log(`Image is the same: ${newImageName}`);
191
+ libdragonInfo.showStatus &&
192
+ log(`Image is the same: ${newImageName}`, true);
190
193
  return false;
191
194
  }
192
195
  } else {
193
196
  await download();
194
197
  }
195
198
 
196
- libdragonInfo.showStatus && log(`Image is different: ${newImageName}`);
199
+ libdragonInfo.showStatus && log(`Image is different: ${newImageName}`, true);
197
200
  return newImageName;
198
201
  };
199
202
 
@@ -301,6 +304,15 @@ async function tryCacheContainerId(libdragonInfo) {
301
304
  }
302
305
  }
303
306
 
307
+ // Throws if the project was not initialized for the current libdragonInfo
308
+ async function mustHaveProject(libdragonInfo) {
309
+ if (!libdragonInfo.haveProjectConfig) {
310
+ throw new ParameterError(
311
+ 'This is not a libdragon project. Initialize with `libdragon init` first.'
312
+ );
313
+ }
314
+ }
315
+
304
316
  module.exports = {
305
317
  installDependencies,
306
318
  updateImage,
@@ -311,4 +323,5 @@ module.exports = {
311
323
  tryCacheContainerId,
312
324
  runGitMaybeHost,
313
325
  findNPMRoot,
326
+ mustHaveProject,
314
327
  };
@@ -13,6 +13,7 @@ module.exports = {
13
13
  STATUS_OK: 0,
14
14
  STATUS_ERROR: 1,
15
15
  STATUS_BAD_PARAM: 2,
16
+ STATUS_VALIDATION_ERROR: 4,
16
17
 
17
18
  IMAGE_FILE: 'docker-image', // deprecated
18
19
  };
@@ -6,6 +6,7 @@ const { spawn } = require('child_process');
6
6
 
7
7
  const { globals } = require('./globals');
8
8
 
9
+ // An error caused by a command explicitly run by the user
9
10
  class CommandError extends Error {
10
11
  constructor(message, { code, out, userCommand }) {
11
12
  super(message);
@@ -15,12 +16,20 @@ class CommandError extends Error {
15
16
  }
16
17
  }
17
18
 
19
+ // The user provided an unexpected input
18
20
  class ParameterError extends Error {
19
21
  constructor(message) {
20
22
  super(message);
21
23
  }
22
24
  }
23
25
 
26
+ // Something was not as expected to continue the operation
27
+ class ValidationError extends Error {
28
+ constructor(message) {
29
+ super(message);
30
+ }
31
+ }
32
+
24
33
  async function fileExists(path) {
25
34
  return fs
26
35
  .stat(path)
@@ -210,6 +219,7 @@ function assert(condition, error) {
210
219
  }
211
220
  }
212
221
 
222
+ // TODO: we can handle showStatus here
213
223
  function log(text, verboseOnly = false) {
214
224
  if (!verboseOnly) {
215
225
  // eslint-disable-next-line no-console
@@ -234,4 +244,5 @@ module.exports = {
234
244
  copyDirContents,
235
245
  CommandError,
236
246
  ParameterError,
247
+ ValidationError,
237
248
  };
@@ -20,13 +20,7 @@ const {
20
20
  CONTAINER_TARGET_PATH,
21
21
  } = require('./constants');
22
22
 
23
- const {
24
- fileExists,
25
- log,
26
- spawnProcess,
27
- dirExists,
28
- toPosixPath,
29
- } = require('./helpers');
23
+ const { fileExists, log, spawnProcess, toPosixPath } = require('./helpers');
30
24
 
31
25
  async function findContainerId(libdragonInfo) {
32
26
  const idFile = path.join(libdragonInfo.root, '.git', CACHED_CONTAINER_FILE);
@@ -67,8 +61,8 @@ async function findContainerId(libdragonInfo) {
67
61
  }
68
62
 
69
63
  async function findLibdragonRoot(start = '.') {
70
- const manifest = path.join(start, LIBDRAGON_PROJECT_MANIFEST);
71
- if (await dirExists(manifest)) {
64
+ const manifest = path.join(start, LIBDRAGON_PROJECT_MANIFEST, CONFIG_FILE);
65
+ if (await fileExists(manifest)) {
72
66
  return path.resolve(start);
73
67
  } else {
74
68
  const parent = path.resolve(start, '..');
@@ -89,42 +83,16 @@ async function findGitRoot() {
89
83
  }
90
84
  }
91
85
 
92
- /**
93
- * Creates the manifest folder if it does not exist. Will return true if
94
- * created, false otherwise.
95
- */
96
- async function createManifestIfNotExist(libdragonInfo) {
97
- const manifestPath = path.join(
98
- libdragonInfo.root,
99
- LIBDRAGON_PROJECT_MANIFEST
100
- );
101
- const manifestExists = await fs.stat(manifestPath).catch((e) => {
102
- if (e.code !== 'ENOENT') throw e;
103
- return false;
104
- });
105
-
106
- if (manifestExists && !manifestExists.isDirectory()) {
107
- throw new Error(
108
- 'There is already a `.libdragon` file and it is not a directory.'
109
- );
110
- }
111
-
112
- if (!manifestExists) {
113
- log(
114
- `Creating libdragon project configuration at \`${libdragonInfo.root}\`.`
115
- );
116
- await fs.mkdir(manifestPath);
117
- }
118
- }
119
-
120
86
  async function readProjectInfo() {
87
+ const projectRoot = await findLibdragonRoot();
88
+
121
89
  let info = {
122
- root:
123
- (await findLibdragonRoot()) ??
124
- (await findNPMRoot()) ??
125
- (await findGitRoot()),
90
+ root: projectRoot ?? (await findNPMRoot()) ?? (await findGitRoot()),
126
91
  userInfo: os.userInfo(),
127
92
 
93
+ // Use this to discriminate if there is a project when the command is run
94
+ haveProjectConfig: !!projectRoot,
95
+
128
96
  // Set the defaults immediately, these should be present at all times even
129
97
  // if we are migrating from the old config because they did not exist before
130
98
  imageName: DOCKER_HUB_IMAGE,
@@ -200,27 +168,41 @@ let projectInfoToWrite = {};
200
168
  * Updates project info to be written. The provided keys are overwritten without
201
169
  * changing the existing values. When the process exists successfully these will
202
170
  * get written to the configuration file. Echoes back the given info.
203
- * @param libdragonInfo
171
+ * @param info This is only the base info without action properties like showStatus
172
+ * fn and command line options
204
173
  */
205
- function setProjectInfoToSave(libdragonInfo) {
206
- projectInfoToWrite = { ...projectInfoToWrite, ...libdragonInfo };
207
- return libdragonInfo;
174
+ function setProjectInfoToSave(info) {
175
+ projectInfoToWrite = { ...projectInfoToWrite, ...info };
176
+ return info;
208
177
  }
209
178
 
210
- async function writeProjectInfo(libdragonInfo = projectInfoToWrite) {
211
- await createManifestIfNotExist(libdragonInfo);
212
- const manifestPath = path.join(
213
- libdragonInfo.root,
214
- LIBDRAGON_PROJECT_MANIFEST
215
- );
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) {
184
+ // Do not log anything here as it may litter the output being always run on exit
185
+
186
+ const projectPath = path.join(info.root, LIBDRAGON_PROJECT_MANIFEST);
187
+
188
+ const pathExists = await fs.stat(projectPath).catch((e) => {
189
+ if (e.code !== 'ENOENT') throw e;
190
+ return false;
191
+ });
192
+
193
+ if (!pathExists) {
194
+ log(`Creating libdragon project configuration at \`${info.root}\`.`, true);
195
+ await fs.mkdir(projectPath);
196
+ }
197
+
216
198
  await fs.writeFile(
217
- path.join(manifestPath, CONFIG_FILE),
199
+ path.join(projectPath, CONFIG_FILE),
218
200
  JSON.stringify(
219
201
  {
220
- imageName: libdragonInfo.imageName,
202
+ imageName: info.imageName,
221
203
  // Always save this in posix format
222
- vendorDirectory: toPosixPath(libdragonInfo.vendorDirectory),
223
- vendorStrategy: libdragonInfo.vendorStrategy,
204
+ vendorDirectory: toPosixPath(info.vendorDirectory),
205
+ vendorStrategy: info.vendorStrategy,
224
206
  },
225
207
  null,
226
208
  ' '
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libdragon",
3
- "version": "10.4.1",
3
+ "version": "10.4.2",
4
4
  "description": "This is a docker wrapper for libdragon",
5
5
  "main": "index.js",
6
6
  "engines": {