diff --git a/ci/test.py b/ci/test.py index 7b58548..27aa912 100755 --- a/ci/test.py +++ b/ci/test.py @@ -608,6 +608,58 @@ def test_malformed_bind_dest(self): s = self.assertPyrexContainerShellCommand("true", capture=True, returncode=1) self.assertIn("Error: too many colons in run.bind entry '%s'." % b, s) + def test_duplicate_bind(self): + temp_dir = tempfile.mkdtemp("-pyrex") + self.addCleanup(shutil.rmtree, temp_dir) + + conf = self.get_config() + conf["run"]["bind"] += " %s" % (temp_dir) + conf["run"]["bind"] += " %s" % (temp_dir) + conf.write_conf() + + self.assertPyrexContainerShellCommand( + "test -e {dir}".format(dir=temp_dir), returncode=0 + ) + + def test_equivalent_bind(self): + temp_dir = tempfile.mkdtemp("-pyrex") + self.addCleanup(shutil.rmtree, temp_dir) + + conf = self.get_config() + conf["run"]["bind"] += " %s" % (temp_dir) + conf["run"]["bind"] += " %s:%s" % (temp_dir, temp_dir) + conf.write_conf() + + self.assertPyrexContainerShellCommand( + "test -e {dir}".format(dir=temp_dir), returncode=0 + ) + + def test_conflicting_bind_sources(self): + temp_dir1 = tempfile.mkdtemp("-pyrex") + self.addCleanup(shutil.rmtree, temp_dir1) + + temp_dir2 = tempfile.mkdtemp("-pyrex") + self.addCleanup(shutil.rmtree, temp_dir2) + + conf = self.get_config() + conf["run"]["bind"] += " %s:/dst" % (temp_dir1) + conf["run"]["bind"] += " %s:/dst" % (temp_dir2) + conf.write_conf() + + s = self.assertPyrexContainerShellCommand("true", capture=True, returncode=1) + self.assertIn("Error: more than one bind for same destination path", s) + + def test_conflicting_bind_options(self): + conf = self.get_config() + conf["run"]["bind"] += " /foo,optional" + conf["run"]["bind"] += " /foo" + conf.write_conf() + + s = self.assertPyrexContainerShellCommand( + "test -e /foo", capture=True, returncode=1 + ) + self.assertIn("Error: more than one bind for same destination path", s) + def test_bad_confversion(self): # Verify that a bad config is an error conf = self.get_config() diff --git a/pyrex.py b/pyrex.py index 43d6952..a791041 100755 --- a/pyrex.py +++ b/pyrex.py @@ -369,6 +369,17 @@ def parse_bind_options(bind): return src, dst, options +def prettyprint_bind(src, dst, options): + options_keys = list(vars(options).keys()) + options_keys.sort() + options_str = "" + for k in options_keys: + if getattr(options, k): + options_str = options_str + ",{option}".format(option=k) + + return "{src}:{dst}{options}".format(src=src, dst=dst, options=options_str) + + def prep_container( config, build_config, @@ -547,13 +558,32 @@ def prep_container( + os.environ.get("PYREX_CONFIG_BIND", "").split() + extra_bind ) - for b in set(binds): + + binds_by_dst = {} + prettyprinted_binds_by_dst = {} + + for b in binds: try: src, dst, options = parse_bind_options(b) except ParsingError as e: print("Error: %s" % e) return [] + if dst in binds_by_dst: + # Allow identical entries + if prettyprint_bind(src, dst, options) == prettyprinted_binds_by_dst[dst]: + continue + else: + print( + "Error: more than one bind for same destination path {dst}: '{bind1}' and '{bind2}'".format( + dst=dst, bind1=binds_by_dst[dst], bind2=b + ) + ) + return [] + else: + binds_by_dst[dst] = b + prettyprinted_binds_by_dst[dst] = prettyprint_bind(src, dst, options) + if not os.path.exists(src): if options.optional: continue