blob: 4685805e4761a2442150121bff0615cfabd0c3ab [file] [log] [blame]
Mike Frysinger1dad0972019-02-23 18:36:37 -05001// Copyright 2019 The Chromium OS Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "dev-install/dev_install.h"
6
Mike Frysinger60260f62019-02-24 02:28:23 -05007#include <unistd.h>
8
9#include <istream>
10#include <sstream>
11#include <string>
12
Mike Frysingerb9c9f6c2019-02-24 02:32:32 -050013#include <base/files/file_path.h>
14#include <base/files/file_util.h>
15#include <base/files/scoped_temp_dir.h>
Mike Frysinger1dad0972019-02-23 18:36:37 -050016#include <gmock/gmock.h>
17#include <gtest/gtest.h>
18
19using ::testing::_;
20using ::testing::Return;
21
22namespace dev_install {
23
24namespace {
25
26class DevInstallMock : public DevInstall {
27 public:
Ben Chan1ea2ba12019-09-19 11:58:03 -070028 MOCK_METHOD(int, Exec, (const std::vector<const char*>&), (override));
29 MOCK_METHOD(bool, IsDevMode, (), (const, override));
30 MOCK_METHOD(bool,
31 PromptUser,
32 (std::istream&, const std::string&),
33 (override));
Mike Frysinger69c167f2019-02-24 05:14:57 -050034 MOCK_METHOD(bool, ClearStateDir, (const base::FilePath&), (override));
Mike Frysinger3d44f782019-02-25 23:17:08 -050035 MOCK_METHOD(bool,
36 InitializeStateDir,
37 (const base::FilePath& dir),
38 (override));
Mike Frysingerbf36e3c2019-11-02 02:47:50 -040039 MOCK_METHOD(bool,
40 DownloadAndInstallBootstrapPackages,
41 (const base::FilePath&),
42 (override));
Mike Frysinger6fc79c02019-11-04 16:35:11 -050043 MOCK_METHOD(bool, ConfigurePortage, (), (override));
Mike Frysinger1dad0972019-02-23 18:36:37 -050044};
45
46class DevInstallTest : public ::testing::Test {
Mike Frysingeree5af6e2019-02-23 23:47:03 -050047 public:
48 void SetUp() override {
49 // Set the default to dev mode enabled. Most tests want that.
50 ON_CALL(dev_install_, IsDevMode()).WillByDefault(Return(true));
Mike Frysinger69c167f2019-02-24 05:14:57 -050051
Mike Frysinger3d44f782019-02-25 23:17:08 -050052 // Ignore stateful setup for most tests.
53 ON_CALL(dev_install_, InitializeStateDir(_)).WillByDefault(Return(true));
54
Mike Frysingerbf36e3c2019-11-02 02:47:50 -040055 // Ignore bootstrap for most tests.
56 ON_CALL(dev_install_, DownloadAndInstallBootstrapPackages(_))
57 .WillByDefault(Return(true));
58
Mike Frysinger6fc79c02019-11-04 16:35:11 -050059 // Ignore portage setup for most tests.
60 ON_CALL(dev_install_, ConfigurePortage()).WillByDefault(Return(true));
61
Mike Frysinger69c167f2019-02-24 05:14:57 -050062 // Most tests should run with a path that doesn't exist.
63 dev_install_.SetStateDirForTest(base::FilePath("/.path-does-not-exist"));
Mike Frysingeree5af6e2019-02-23 23:47:03 -050064 }
65
Mike Frysinger1dad0972019-02-23 18:36:37 -050066 protected:
67 DevInstallMock dev_install_;
68};
69
70} // namespace
71
72// Check default run through.
73TEST_F(DevInstallTest, Run) {
74 EXPECT_CALL(dev_install_, Exec(_)).WillOnce(Return(1234));
75 EXPECT_EQ(1234, dev_install_.Run());
76}
77
Mike Frysingeree5af6e2019-02-23 23:47:03 -050078// Systems not in dev mode should abort.
79TEST_F(DevInstallTest, NonDevMode) {
80 EXPECT_CALL(dev_install_, IsDevMode()).WillOnce(Return(false));
Mike Frysinger69c167f2019-02-24 05:14:57 -050081 EXPECT_CALL(dev_install_, ClearStateDir(_)).Times(0);
Mike Frysingeree5af6e2019-02-23 23:47:03 -050082 EXPECT_CALL(dev_install_, Exec(_)).Times(0);
83 EXPECT_EQ(2, dev_install_.Run());
84}
85
Mike Frysinger69c167f2019-02-24 05:14:57 -050086// Check system has been initialized.
87TEST_F(DevInstallTest, AlreadyInitialized) {
88 dev_install_.SetStateDirForTest(base::FilePath("/"));
89 EXPECT_CALL(dev_install_, Exec(_)).Times(0);
90 ASSERT_EQ(4, dev_install_.Run());
91}
92
93// Check --reinstall passed.
94TEST_F(DevInstallTest, RunReinstallWorked) {
95 dev_install_.SetReinstallForTest(true);
96 EXPECT_CALL(dev_install_, ClearStateDir(_)).WillOnce(Return(true));
97 EXPECT_CALL(dev_install_, Exec(_)).WillOnce(Return(1234));
98 ASSERT_EQ(1234, dev_install_.Run());
99}
100
101// Check when --reinstall is requested but clearing fails.
102TEST_F(DevInstallTest, RunReinstallFails) {
103 dev_install_.SetReinstallForTest(true);
104 EXPECT_CALL(dev_install_, ClearStateDir(_)).WillOnce(Return(false));
105 EXPECT_CALL(dev_install_, Exec(_)).Times(0);
106 ASSERT_EQ(1, dev_install_.Run());
107}
108
109// Check --uninstall passed.
110TEST_F(DevInstallTest, RunUninstall) {
111 dev_install_.SetUninstallForTest(true);
112 EXPECT_CALL(dev_install_, ClearStateDir(_)).WillOnce(Return(true));
113 EXPECT_CALL(dev_install_, Exec(_)).Times(0);
114 ASSERT_EQ(0, dev_install_.Run());
115}
116
Mike Frysinger3d44f782019-02-25 23:17:08 -0500117// Stateful setup failures.
118TEST_F(DevInstallTest, StatefulSetupFailure) {
119 EXPECT_CALL(dev_install_, InitializeStateDir(_)).WillOnce(Return(false));
120 EXPECT_CALL(dev_install_, Exec(_)).Times(0);
121 ASSERT_EQ(5, dev_install_.Run());
122}
123
Mike Frysingerbf36e3c2019-11-02 02:47:50 -0400124// We only bootstrap before exiting.
125TEST_F(DevInstallTest, BootstrapOnly) {
126 dev_install_.SetBootstrapForTest(true);
127 EXPECT_CALL(dev_install_, Exec(_)).Times(0);
128 ASSERT_EQ(0, dev_install_.Run());
129}
130
131// Bootstrap failures.
132TEST_F(DevInstallTest, BootstrapFailure) {
133 EXPECT_CALL(dev_install_, DownloadAndInstallBootstrapPackages(_))
134 .WillOnce(Return(false));
135 EXPECT_CALL(dev_install_, Exec(_)).Times(0);
136 ASSERT_EQ(7, dev_install_.Run());
137}
138
Mike Frysinger6fc79c02019-11-04 16:35:11 -0500139// Portage setup failures.
140TEST_F(DevInstallTest, PortageFailure) {
141 EXPECT_CALL(dev_install_, ConfigurePortage()).WillOnce(Return(false));
142 EXPECT_CALL(dev_install_, Exec(_)).Times(0);
143 ASSERT_EQ(8, dev_install_.Run());
144}
145
Mike Frysinger60260f62019-02-24 02:28:23 -0500146namespace {
147
148class PromptUserTest : public ::testing::Test {
149 protected:
150 DevInstall dev_install_;
151};
152
153} // namespace
154
155// The --yes flag should pass w/out prompting the user.
156TEST_F(PromptUserTest, Forced) {
157 dev_install_.SetYesForTest(true);
158 std::stringstream stream("");
159 EXPECT_TRUE(dev_install_.PromptUser(stream, ""));
160}
161
162// EOF input should fail.
163TEST_F(PromptUserTest, Eof) {
164 std::stringstream stream("");
165 EXPECT_FALSE(dev_install_.PromptUser(stream, ""));
166}
167
168// Default input (hitting enter) should fail.
169TEST_F(PromptUserTest, Default) {
170 std::stringstream stream("\n");
171 EXPECT_FALSE(dev_install_.PromptUser(stream, ""));
172}
173
174// Entering "n" should fail.
175TEST_F(PromptUserTest, No) {
176 std::stringstream stream("n\n");
177 EXPECT_FALSE(dev_install_.PromptUser(stream, ""));
178}
179
180// Entering "y" should pass.
181TEST_F(PromptUserTest, Yes) {
182 std::stringstream stream("y\n");
183 EXPECT_TRUE(dev_install_.PromptUser(stream, ""));
184}
185
Mike Frysingerb9c9f6c2019-02-24 02:32:32 -0500186namespace {
187
188class DeletePathTest : public ::testing::Test {
189 public:
190 void SetUp() override {
191 ASSERT_TRUE(scoped_temp_dir_.CreateUniqueTempDir());
192 test_dir_ = scoped_temp_dir_.GetPath();
193 dev_install_.SetStateDirForTest(test_dir_);
194 }
195
196 protected:
197 DevInstall dev_install_;
198 base::FilePath test_dir_;
199 base::ScopedTempDir scoped_temp_dir_;
200};
201
202} // namespace
203
204// Check missing dir.
205TEST_F(DeletePathTest, Missing) {
206 struct stat st = {};
207 EXPECT_TRUE(dev_install_.DeletePath(st, test_dir_.Append("foo")));
208}
209
210// Check deleting dir contents leaves the dir alone.
211TEST_F(DeletePathTest, Empty) {
212 struct stat st = {};
213 EXPECT_TRUE(dev_install_.DeletePath(st, test_dir_));
214 EXPECT_TRUE(base::PathExists(test_dir_));
215}
216
217// Check mounted deletion.
218TEST_F(DeletePathTest, Mounted) {
219 struct stat st = {};
220 const base::FilePath subdir = test_dir_.Append("subdir");
221 EXPECT_TRUE(base::CreateDirectory(subdir));
222 EXPECT_FALSE(dev_install_.DeletePath(st, test_dir_));
223 EXPECT_TRUE(base::PathExists(subdir));
224}
225
226// Check recursive deletion.
227TEST_F(DeletePathTest, Works) {
228 struct stat st;
229 EXPECT_EQ(0, stat(test_dir_.value().c_str(), &st));
230
231 EXPECT_EQ(3, base::WriteFile(test_dir_.Append("file"), "123", 3));
232 EXPECT_EQ(0, symlink("x", test_dir_.Append("broken-sym").value().c_str()));
233 EXPECT_EQ(0, symlink("file", test_dir_.Append("file-sym").value().c_str()));
234 EXPECT_EQ(0, symlink(".", test_dir_.Append("dir-sym").value().c_str()));
235 EXPECT_EQ(0, symlink("subdir", test_dir_.Append("dir-sym2").value().c_str()));
236 const base::FilePath subdir = test_dir_.Append("subdir");
237 EXPECT_TRUE(base::CreateDirectory(subdir));
238 EXPECT_EQ(3, base::WriteFile(subdir.Append("file"), "123", 3));
239 const base::FilePath subsubdir = test_dir_.Append("subdir");
240 EXPECT_TRUE(base::CreateDirectory(subsubdir));
241 EXPECT_EQ(3, base::WriteFile(subsubdir.Append("file"), "123", 3));
242
243 EXPECT_TRUE(dev_install_.DeletePath(st, test_dir_));
244 EXPECT_TRUE(base::PathExists(test_dir_));
245 EXPECT_EQ(0, rmdir(test_dir_.value().c_str()));
246}
247
Mike Frysinger69c167f2019-02-24 05:14:57 -0500248namespace {
249
Mike Frysinger3eaab632019-11-03 20:19:09 -0500250class CreateMissingDirectoryTest : public ::testing::Test {
251 public:
252 void SetUp() {
253 ASSERT_TRUE(scoped_temp_dir_.CreateUniqueTempDir());
254 test_dir_ = scoped_temp_dir_.GetPath();
255 }
256
257 protected:
258 DevInstall dev_install_;
259 base::FilePath test_dir_;
260 base::ScopedTempDir scoped_temp_dir_;
261};
262
263} // namespace
264
265// Create dirs that don't yet exist.
266TEST_F(CreateMissingDirectoryTest, Works) {
267 const base::FilePath dir = test_dir_.Append("test");
268 ASSERT_TRUE(dev_install_.CreateMissingDirectory(dir));
269 int mode;
270 ASSERT_TRUE(base::GetPosixFilePermissions(dir, &mode));
271 ASSERT_EQ(0755, mode);
272 ASSERT_TRUE(dev_install_.CreateMissingDirectory(dir));
273}
274
275// If a dir already exists, should do nothing.
276TEST_F(CreateMissingDirectoryTest, Existing) {
277 ASSERT_TRUE(dev_install_.CreateMissingDirectory(test_dir_));
278 ASSERT_TRUE(dev_install_.CreateMissingDirectory(test_dir_));
279}
280
281namespace {
282
Mike Frysinger69c167f2019-02-24 05:14:57 -0500283// We could mock out DeletePath, but it's easy to lightly validate it.
284class ClearStateDirMock : public DevInstall {
285 public:
286 MOCK_METHOD(bool,
287 PromptUser,
288 (std::istream&, const std::string&),
289 (override));
290};
291
292class ClearStateDirTest : public ::testing::Test {
293 public:
294 void SetUp() {
295 ASSERT_TRUE(scoped_temp_dir_.CreateUniqueTempDir());
296 test_dir_ = scoped_temp_dir_.GetPath();
297 }
298
299 protected:
300 ClearStateDirMock dev_install_;
301 base::FilePath test_dir_;
302 base::ScopedTempDir scoped_temp_dir_;
303};
304
305} // namespace
306
307// Check user rejecting things.
308TEST_F(ClearStateDirTest, Cancel) {
309 EXPECT_CALL(dev_install_, PromptUser(_, _)).WillOnce(Return(false));
310 const base::FilePath subdir = test_dir_.Append("subdir");
311 ASSERT_TRUE(base::CreateDirectory(subdir));
312 ASSERT_FALSE(dev_install_.ClearStateDir(test_dir_));
313 ASSERT_TRUE(base::PathExists(subdir));
314}
315
316// Check missing dir is handled.
317TEST_F(ClearStateDirTest, Missing) {
318 EXPECT_CALL(dev_install_, PromptUser(_, _)).WillOnce(Return(true));
319 ASSERT_TRUE(dev_install_.ClearStateDir(test_dir_.Append("subdir")));
320 ASSERT_TRUE(base::PathExists(test_dir_));
321}
322
323// Check empty dir is handled.
324TEST_F(ClearStateDirTest, Empty) {
325 EXPECT_CALL(dev_install_, PromptUser(_, _)).WillOnce(Return(true));
326 ASSERT_TRUE(dev_install_.ClearStateDir(test_dir_));
327 ASSERT_TRUE(base::PathExists(test_dir_));
328}
329
330// Check dir with contents is cleared.
331TEST_F(ClearStateDirTest, Works) {
332 EXPECT_CALL(dev_install_, PromptUser(_, _)).WillOnce(Return(true));
333 const base::FilePath subdir = test_dir_.Append("subdir");
334 ASSERT_TRUE(base::CreateDirectory(subdir));
335 ASSERT_TRUE(dev_install_.ClearStateDir(test_dir_));
336 ASSERT_FALSE(base::PathExists(subdir));
337}
338
Mike Frysinger3d44f782019-02-25 23:17:08 -0500339namespace {
340
341class InitializeStateDirTest : public ::testing::Test {
342 public:
343 void SetUp() {
344 ASSERT_TRUE(scoped_temp_dir_.CreateUniqueTempDir());
345 test_dir_ = scoped_temp_dir_.GetPath();
346 }
347
348 protected:
349 DevInstall dev_install_;
350 base::FilePath test_dir_;
351 base::ScopedTempDir scoped_temp_dir_;
352};
353
354} // namespace
355
356// Check stateful is set up correctly.
357TEST_F(InitializeStateDirTest, Works) {
358 // Make sure we fully set things up.
359 ASSERT_TRUE(dev_install_.InitializeStateDir(test_dir_));
360 ASSERT_TRUE(base::IsLink(test_dir_.Append("usr")));
361 ASSERT_TRUE(base::IsLink(test_dir_.Append("local")));
362 ASSERT_TRUE(base::IsLink(test_dir_.Append("local")));
363 const base::FilePath etc = test_dir_.Append("etc");
364 ASSERT_TRUE(base::PathExists(etc));
365 ASSERT_TRUE(base::IsLink(etc.Append("passwd")));
366 ASSERT_TRUE(base::IsLink(etc.Append("group")));
Mike Frysinger6fc79c02019-11-04 16:35:11 -0500367 const base::FilePath tmp = test_dir_.Append("tmp");
368 ASSERT_TRUE(base::PathExists(tmp));
369 // Can't use base::GetPosixFilePermissions as that blocks +t mode.
370 struct stat st;
371 ASSERT_EQ(0, stat(tmp.value().c_str(), &st));
372 ASSERT_EQ(01777, st.st_mode & 07777);
Mike Frysinger3d44f782019-02-25 23:17:08 -0500373
374 // Calling a second time should be fine.
375 ASSERT_TRUE(dev_install_.InitializeStateDir(test_dir_));
376}
377
378// Check we handle errors gracefully.
379TEST_F(InitializeStateDirTest, Fails) {
380 // Create a broken /etc symlink.
381 ASSERT_TRUE(
382 base::CreateSymbolicLink(base::FilePath("foo"), test_dir_.Append("etc")));
383 ASSERT_FALSE(dev_install_.InitializeStateDir(test_dir_));
384}
385
Mike Frysingerb4aadcd2019-02-25 23:50:05 -0500386namespace {
387
388class LoadRuntimeSettingsTest : public ::testing::Test {
389 public:
390 void SetUp() {
391 ASSERT_TRUE(scoped_temp_dir_.CreateUniqueTempDir());
392 test_dir_ = scoped_temp_dir_.GetPath();
393 }
394
395 protected:
396 DevInstall dev_install_;
397 base::FilePath test_dir_;
398 base::ScopedTempDir scoped_temp_dir_;
399};
400
401} // namespace
402
403// Check loading state works.
404TEST_F(LoadRuntimeSettingsTest, Works) {
405 const base::FilePath lsb_release = test_dir_.Append("lsb-release");
406 std::string data{
407 "CHROMEOS_DEVSERVER=https://foo\n"
408 "CHROMEOS_RELEASE_BOARD=betty\n"
409 "CHROMEOS_RELEASE_CHROME_MILESTONE=79\n"
410 "CHROMEOS_RELEASE_VERSION=100.10.1\n"};
411 ASSERT_EQ(base::WriteFile(lsb_release, data.c_str(), data.size()),
412 data.size());
413 ASSERT_TRUE(dev_install_.LoadRuntimeSettings(lsb_release));
414 ASSERT_EQ(dev_install_.GetDevserverUrlForTest(), "https://foo");
415 ASSERT_EQ(dev_install_.GetBoardForTest(), "betty");
416 ASSERT_EQ(dev_install_.GetBinhostVersionForTest(), "100.10.1");
417}
418
419// Check loading empty state works.
420TEST_F(LoadRuntimeSettingsTest, Empty) {
421 const base::FilePath lsb_release = test_dir_.Append("lsb-release");
422 std::string data{""};
423 ASSERT_EQ(base::WriteFile(lsb_release, data.c_str(), data.size()),
424 data.size());
425 ASSERT_TRUE(dev_install_.LoadRuntimeSettings(lsb_release));
426}
427
428// Check loading state doesn't abort with missing file.
429TEST_F(LoadRuntimeSettingsTest, Missing) {
430 ASSERT_TRUE(dev_install_.LoadRuntimeSettings(test_dir_.Append("asdf")));
431}
432
Mike Frysingerbf36e3c2019-11-02 02:47:50 -0400433namespace {
434
435class BootstrapPackagesMock : public DevInstall {
436 public:
437 MOCK_METHOD(bool,
438 DownloadAndInstallBootstrapPackage,
439 (const std::string&),
440 (override));
441};
442
443class BootstrapPackagesTest : public ::testing::Test {
444 public:
445 void SetUp() {
446 // Have the install step pass by default.
447 ON_CALL(dev_install_, DownloadAndInstallBootstrapPackage(_))
448 .WillByDefault(Return(true));
449
450 ASSERT_TRUE(scoped_temp_dir_.CreateUniqueTempDir());
451 test_dir_ = scoped_temp_dir_.GetPath();
452 dev_install_.SetStateDirForTest(test_dir_);
453 }
454
455 protected:
456 BootstrapPackagesMock dev_install_;
457 base::FilePath test_dir_;
458 base::ScopedTempDir scoped_temp_dir_;
459};
460
461} // namespace
462
463// Check bootstrap works in general.
464TEST_F(BootstrapPackagesTest, Works) {
465 const base::FilePath listing = test_dir_.Append("bootstrap.packages");
466 std::string data{
467 "foo/bar-123\n"
468 "cat/pkg-1.0\n"};
469 ASSERT_EQ(base::WriteFile(listing, data.c_str(), data.size()), data.size());
470
471 ON_CALL(dev_install_, DownloadAndInstallBootstrapPackage(_))
472 .WillByDefault(Return(false));
473 EXPECT_CALL(dev_install_, DownloadAndInstallBootstrapPackage("foo/bar-123"))
474 .WillOnce(Return(true));
475 EXPECT_CALL(dev_install_, DownloadAndInstallBootstrapPackage("cat/pkg-1.0"))
476 .WillOnce(Return(true));
477
478 const base::FilePath bindir = test_dir_.Append("usr/bin");
479 ASSERT_TRUE(base::CreateDirectory(bindir));
480 ASSERT_TRUE(dev_install_.DownloadAndInstallBootstrapPackages(listing));
481
482 // We assert the symlinks exist. We assume the targets are valid for now.
483 base::FilePath target;
484 ASSERT_TRUE(base::ReadSymbolicLink(bindir.Append("python"), &target));
485 ASSERT_TRUE(base::ReadSymbolicLink(bindir.Append("python2"), &target));
486 ASSERT_TRUE(base::ReadSymbolicLink(bindir.Append("python3"), &target));
487}
488
489// Check missing bootstrap list fails.
490TEST_F(BootstrapPackagesTest, Missing) {
491 const base::FilePath listing = test_dir_.Append("bootstrap.packages");
492 ASSERT_FALSE(dev_install_.DownloadAndInstallBootstrapPackages(listing));
493}
494
495// Check empty bootstrap list fails.
496TEST_F(BootstrapPackagesTest, Empty) {
497 const base::FilePath listing = test_dir_.Append("bootstrap.packages");
498 ASSERT_EQ(base::WriteFile(listing, "", 0), 0);
499 ASSERT_FALSE(dev_install_.DownloadAndInstallBootstrapPackages(listing));
500}
501
502// Check mid-bootstrap failure behavior.
503TEST_F(BootstrapPackagesTest, PackageFailed) {
504 const base::FilePath listing = test_dir_.Append("bootstrap.packages");
505 std::string data{"cat/pkg-3"};
506 ASSERT_EQ(base::WriteFile(listing, data.c_str(), data.size()), data.size());
507
508 EXPECT_CALL(dev_install_, DownloadAndInstallBootstrapPackage("cat/pkg-3"))
509 .WillOnce(Return(false));
510
511 const base::FilePath bindir = test_dir_.Append("usr/bin");
512 ASSERT_TRUE(base::CreateDirectory(bindir));
513 ASSERT_FALSE(dev_install_.DownloadAndInstallBootstrapPackages(listing));
514}
515
Mike Frysinger6fc79c02019-11-04 16:35:11 -0500516namespace {
517
518class ConfigurePortageTest : public ::testing::Test {
519 public:
520 void SetUp() {
521 ASSERT_TRUE(scoped_temp_dir_.CreateUniqueTempDir());
522 test_dir_ = scoped_temp_dir_.GetPath();
523 dev_install_.SetStateDirForTest(test_dir_);
524 }
525
526 protected:
527 BootstrapPackagesMock dev_install_;
528 base::FilePath test_dir_;
529 base::ScopedTempDir scoped_temp_dir_;
530};
531
532} // namespace
533
534// Check setup works in general.
535TEST_F(ConfigurePortageTest, Works) {
536 EXPECT_TRUE(dev_install_.ConfigurePortage());
537 const base::FilePath portage_dir = test_dir_.Append("etc/portage");
538 EXPECT_TRUE(base::PathExists(portage_dir));
539}
540
Mike Frysinger1dad0972019-02-23 18:36:37 -0500541} // namespace dev_install