Skip to content

Commit 5c6aec6

Browse files
authored
install: Add --wipe_destdir option (#894)
* install: test uses pathlib. Cleans up a lot of code! * install: add --wipe_destdir option. If specified, wipe destination directory before installing. Fixes #893. * install: Update doc for --wipe_destdir. Clarify that this will delete the whole directory. --------- Co-authored-by: HONG Yifan <[email protected]>
1 parent 6a9eaf2 commit 5c6aec6

File tree

2 files changed

+50
-27
lines changed

2 files changed

+50
-27
lines changed

pkg/private/install.py.tpl

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,12 @@ RUNFILE_PREFIX = os.path.join(os.getenv("RUNFILES_DIR"), WORKSPACE_NAME) if os.g
5353
#
5454
# See also https://bugs.python.org/issue37157.
5555
class NativeInstaller(object):
56-
def __init__(self, default_user=None, default_group=None, destdir=None):
56+
def __init__(self, default_user=None, default_group=None, destdir=None,
57+
wipe_destdir=False):
5758
self.default_user = default_user
5859
self.default_group = default_group
5960
self.destdir = destdir
61+
self.wipe_destdir = wipe_destdir
6062
self.entries = []
6163

6264
# Logger helper method, may not be necessary or desired
@@ -171,6 +173,9 @@ class NativeInstaller(object):
171173

172174
def do_the_thing(self):
173175
logging.info("Installing to %s", self.destdir)
176+
if self.wipe_destdir:
177+
logging.debug("RM %s", self.destdir)
178+
shutil.rmtree(self.destdir, ignore_errors=True)
174179
for entry in self.entries:
175180
if entry.type == manifest.ENTRY_IS_FILE:
176181
self._install_file(entry)
@@ -236,6 +241,9 @@ def main(args):
236241
f"Relative paths are interpreted against "
237242
f"BUILD_WORKSPACE_DIRECTORY "
238243
f"({os.getenv('BUILD_WORKSPACE_DIRECTORY')})")
244+
parser.add_argument("--wipe_destdir", action="store_true", default=False,
245+
help="Delete destdir tree (including destdir itself) "
246+
"before installing")
239247

240248
args = parser.parse_args()
241249

@@ -251,7 +259,10 @@ def main(args):
251259
level=level, format="%(levelname)s: %(message)s"
252260
)
253261

254-
installer = NativeInstaller(destdir=args.destdir)
262+
installer = NativeInstaller(
263+
destdir=args.destdir,
264+
wipe_destdir=args.wipe_destdir,
265+
)
255266

256267
if not CALLED_FROM_BAZEL_RUN and RUNFILE_PREFIX is None:
257268
logging.critical("RUNFILES_DIR must be set in your environment when this is run as a bazel build tool.")

tests/install/test.py

Lines changed: 37 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
# See the License for the specific language governing permissions and
1515
# limitations under the License.
1616

17-
import itertools
1817
import os
18+
import pathlib
1919
import unittest
2020
import stat
2121
import subprocess
@@ -24,7 +24,7 @@
2424
from pkg.private import manifest
2525

2626

27-
class PkgInstallTest(unittest.TestCase):
27+
class PkgInstallTestBase(unittest.TestCase):
2828
@classmethod
2929
def setUpClass(cls):
3030
cls.runfiles = runfiles.Create()
@@ -36,8 +36,14 @@ def setUpClass(cls):
3636
cls.manifest_data = {}
3737

3838
for entry in manifest_entries:
39-
cls.manifest_data[entry.dest] = entry
40-
cls.installdir = os.path.join(os.getenv("TEST_TMPDIR"), "installdir")
39+
cls.manifest_data[pathlib.Path(entry.dest)] = entry
40+
cls.installdir = pathlib.Path(os.getenv("TEST_TMPDIR")) / "installdir"
41+
42+
43+
class PkgInstallTest(PkgInstallTestBase):
44+
@classmethod
45+
def setUpClass(cls):
46+
super().setUpClass()
4147
env = {}
4248
env.update(cls.runfiles.EnvVars())
4349
subprocess.check_call([
@@ -48,11 +54,11 @@ def setUpClass(cls):
4854
env=env)
4955

5056
def entity_type_at_path(self, path):
51-
if os.path.islink(path):
57+
if path.is_symlink():
5258
return manifest.ENTRY_IS_LINK
53-
elif os.path.isfile(path):
59+
elif path.is_file():
5460
return manifest.ENTRY_IS_FILE
55-
elif os.path.isdir(path):
61+
elif path.is_dir():
5662
return manifest.ENTRY_IS_DIR
5763
else:
5864
# We can't infer what TreeArtifacts are by looking at them -- the
@@ -97,10 +103,13 @@ def assertEntryModeMatches(self, entry, actual_path,
97103

98104
def _find_tree_entry(self, path, owned_trees):
99105
for tree_root in owned_trees:
100-
if path == tree_root or path.startswith(tree_root + "/"):
106+
if self._path_starts_with(path, tree_root):
101107
return tree_root
102108
return None
103109

110+
def _path_starts_with(self, path, other):
111+
return path.parts[:len(other.parts)] == other.parts
112+
104113
def test_manifest_matches(self):
105114
unowned_dirs = set()
106115
owned_dirs = set()
@@ -119,28 +128,17 @@ def test_manifest_matches(self):
119128
elif data.type == manifest.ENTRY_IS_TREE:
120129
owned_trees[dest] = data
121130

122-
# TODO(nacl): The initial stage of the accumulation returns an empty string,
123-
# which end up in the set representing the root of the manifest.
124-
# This may not be the best thing.
125-
unowned_dirs.update([p for p in itertools.accumulate(os.path.dirname(dest).split('/'),
126-
func=lambda accum, new: accum + '/' + new)])
131+
unowned_dirs.update(dest.parents)
127132

128133
# In the above loop, unowned_dirs contains all possible directories that
129134
# are in the manifest. Prune them here.
130135
unowned_dirs -= owned_dirs
131136

132137
# TODO: check for ownership (user, group)
133-
found_entries = {dest: False for dest in self.manifest_data.keys()}
138+
found_entries = {dest: False for dest in self.manifest_data}
134139
for root, dirs, files in os.walk(self.installdir):
135-
rel_root_path = os.path.relpath(root, self.installdir)
136-
137-
# The rest of this uses string comparison. To reduce potential
138-
# confusion, ensure that the "." doesn't show up elsewhere.
139-
#
140-
# TODO(nacl) consider using pathlib here, which will reduce the
141-
# need for path cleverness.
142-
if rel_root_path == '.':
143-
rel_root_path = ''
140+
root = pathlib.Path(root)
141+
rel_root_path = root.relative_to(self.installdir)
144142

145143
# Directory ownership tests
146144
if len(files) == 0 and len(dirs) == 0:
@@ -182,10 +180,10 @@ def test_manifest_matches(self):
182180
# needed. It maybe worth setting the keys in the manifest_data
183181
# dictionary to pathlib.Path or otherwise converting them to
184182
# native paths.
185-
fpath = os.path.normpath("/".join([root, f]))
183+
fpath = root / f
186184
# The path inside the manifest (relative to the install
187185
# destdir).
188-
rel_fpath = os.path.normpath("/".join([rel_root_path, f]))
186+
rel_fpath = rel_root_path / f
189187
entity_in_manifest = rel_fpath in self.manifest_data
190188
entity_tree_root = self._find_tree_entry(rel_fpath, owned_trees)
191189
if not entity_in_manifest and not entity_tree_root:
@@ -211,5 +209,19 @@ def test_manifest_matches(self):
211209
self.assertEqual(num_missing, 0)
212210

213211

212+
class WipeTest(PkgInstallTestBase):
213+
def test_wipe(self):
214+
self.installdir.mkdir(exist_ok=True)
215+
(self.installdir / "should_be_deleted.txt").touch()
216+
217+
subprocess.check_call([
218+
self.runfiles.Rlocation("rules_pkg/tests/install/test_installer"),
219+
"--destdir", self.installdir,
220+
"--wipe_destdir",
221+
],
222+
env=self.runfiles.EnvVars())
223+
self.assertFalse((self.installdir / "should_be_deleted.txt").exists())
224+
225+
214226
if __name__ == "__main__":
215227
unittest.main()

0 commit comments

Comments
 (0)