From 47b3dda1aa92ab98450950ba6383282cbf158857 Mon Sep 17 00:00:00 2001 From: Scott Wittenburg Date: Tue, 12 May 2020 16:16:52 -0600 Subject: Support os-specific $padding in config:install_tree Providing only $padding or ${padding} results in an attempt to substitute a padding of maximum system path length, while leaving room for the parts of the install path spack generates. Providing $padding- or ${padding-} simply substitutes padding of the specified length. --- lib/spack/spack/test/config.py | 46 +++++++++++++++++++------- lib/spack/spack/util/path.py | 73 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 11 deletions(-) diff --git a/lib/spack/spack/test/config.py b/lib/spack/spack/test/config.py index b8598616d5..8212db6c21 100644 --- a/lib/spack/spack/test/config.py +++ b/lib/spack/spack/test/config.py @@ -23,7 +23,7 @@ import spack.schema.packages import spack.schema.mirrors import spack.schema.repos import spack.util.spack_yaml as syaml -from spack.util.path import canonicalize_path +import spack.util.path as spack_path # sample config data @@ -272,31 +272,31 @@ def test_substitute_config_variables(mock_low_high_config): assert os.path.join( '/foo/bar/baz', prefix - ) == canonicalize_path('/foo/bar/baz/$spack') + ) == spack_path.canonicalize_path('/foo/bar/baz/$spack') assert os.path.join( spack.paths.prefix, 'foo/bar/baz' - ) == canonicalize_path('$spack/foo/bar/baz/') + ) == spack_path.canonicalize_path('$spack/foo/bar/baz/') assert os.path.join( '/foo/bar/baz', prefix, 'foo/bar/baz' - ) == canonicalize_path('/foo/bar/baz/$spack/foo/bar/baz/') + ) == spack_path.canonicalize_path('/foo/bar/baz/$spack/foo/bar/baz/') assert os.path.join( '/foo/bar/baz', prefix - ) == canonicalize_path('/foo/bar/baz/${spack}') + ) == spack_path.canonicalize_path('/foo/bar/baz/${spack}') assert os.path.join( spack.paths.prefix, 'foo/bar/baz' - ) == canonicalize_path('${spack}/foo/bar/baz/') + ) == spack_path.canonicalize_path('${spack}/foo/bar/baz/') assert os.path.join( '/foo/bar/baz', prefix, 'foo/bar/baz' - ) == canonicalize_path('/foo/bar/baz/${spack}/foo/bar/baz/') + ) == spack_path.canonicalize_path('/foo/bar/baz/${spack}/foo/bar/baz/') assert os.path.join( '/foo/bar/baz', prefix, 'foo/bar/baz' - ) != canonicalize_path('/foo/bar/baz/${spack/foo/bar/baz/') + ) != spack_path.canonicalize_path('/foo/bar/baz/${spack/foo/bar/baz/') packages_merge_low = { @@ -345,19 +345,43 @@ def test_merge_with_defaults(mock_low_high_config, write_config_file): def test_substitute_user(mock_low_high_config): user = getpass.getuser() - assert '/foo/bar/' + user + '/baz' == canonicalize_path( + assert '/foo/bar/' + user + '/baz' == spack_path.canonicalize_path( '/foo/bar/$user/baz' ) def test_substitute_tempdir(mock_low_high_config): tempdir = tempfile.gettempdir() - assert tempdir == canonicalize_path('$tempdir') - assert tempdir + '/foo/bar/baz' == canonicalize_path( + assert tempdir == spack_path.canonicalize_path('$tempdir') + assert tempdir + '/foo/bar/baz' == spack_path.canonicalize_path( '$tempdir/foo/bar/baz' ) +def test_substitute_padding(mock_low_high_config): + max_system_path = spack_path.get_system_path_max() + expected_length = (max_system_path - + spack_path.SPACK_MAX_INSTALL_PATH_LENGTH) + + install_path = spack_path.canonicalize_path('/foo/bar/${padding}/baz') + + assert spack_path.SPACK_PATH_PADDING_CHARS in install_path + assert len(install_path) == expected_length + + install_path = spack_path.canonicalize_path('/foo/bar/baz/gah/$padding') + + assert spack_path.SPACK_PATH_PADDING_CHARS in install_path + assert len(install_path) == expected_length + + i_path = spack_path.canonicalize_path('/foo/$padding:10') + i_expect = os.path.join('/foo', spack_path.SPACK_PATH_PADDING_CHARS[:10]) + assert i_path == i_expect + + i_path = spack_path.canonicalize_path('/foo/${padding:20}') + i_expect = os.path.join('/foo', spack_path.SPACK_PATH_PADDING_CHARS[:20]) + assert i_path == i_expect + + def test_read_config(mock_low_high_config, write_config_file): write_config_file('config', config_low, 'low') assert spack.config.get('config') == config_low['config'] diff --git a/lib/spack/spack/util/path.py b/lib/spack/spack/util/path.py index 9d5413c609..8bcf598882 100644 --- a/lib/spack/spack/util/path.py +++ b/lib/spack/spack/util/path.py @@ -10,8 +10,12 @@ TODO: this is really part of spack.config. Consolidate it. import os import re import getpass +import subprocess import tempfile +import llnl.util.tty as tty +from llnl.util.lang import memoized + import spack.paths @@ -27,6 +31,38 @@ replacements = { 'tempdir': tempfile.gettempdir(), } +# This is intended to be longer than the part of the install path +# spack generates from the root path we give it. Included in the +# estimate: +# +# os-arch -> 30 +# compiler -> 30 +# package name -> 50 (longest is currently 47 characters) +# version -> 20 +# hash -> 32 +# buffer -> 138 +# --------------------- +# total -> 300 +SPACK_MAX_INSTALL_PATH_LENGTH = 300 +SPACK_PATH_PADDING_CHARS = 'spack_path_placeholder' + + +@memoized +def get_system_path_max(): + # Choose a conservative default + sys_max_path_length = 256 + try: + path_max_proc = subprocess.Popen(['getconf', 'PATH_MAX', '/'], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + proc_output = str(path_max_proc.communicate()[0].decode()) + sys_max_path_length = int(proc_output) + except (ValueError, subprocess.CalledProcessError, OSError): + tty.msg('Unable to find system max path length, using: {0}'.format( + sys_max_path_length)) + + return sys_max_path_length + def substitute_config_variables(path): """Substitute placeholders into paths. @@ -58,8 +94,45 @@ def substitute_path_variables(path): return path +def _get_padding_string(length): + spack_path_padding_size = len(SPACK_PATH_PADDING_CHARS) + num_reps = int(length / (spack_path_padding_size + 1)) + extra_chars = length % (spack_path_padding_size + 1) + reps_list = [SPACK_PATH_PADDING_CHARS for i in range(num_reps)] + reps_list.append(SPACK_PATH_PADDING_CHARS[:extra_chars]) + return os.path.sep.join(reps_list) + + +def _add_computed_padding(path): + """Subtitute in padding of os-specific length. The intent is to leave + SPACK_MAX_INSTALL_PATH_LENGTH characters available for parts of the + path generated by spack. This is to allow for not-completely-known + lengths of things like os/arch, compiler, package name, hash length, + etc. + """ + padding_regex = re.compile(r'(\$[\w\d\:]+\b|\$\{[\w\d\:]+\})') + m = padding_regex.search(path) + if m and m.group(0).strip('${}').startswith('padding'): + padding_part = m.group(0) + len_pad_part = len(m.group(0)) + p_match = re.search(r'\:(\d+)', padding_part) + if p_match: + computed_padding = _get_padding_string(int(p_match.group(1))) + else: + # Take whatever has been computed/substituted so far and add some + # room + path_len = len(path) - len_pad_part + SPACK_MAX_INSTALL_PATH_LENGTH + system_max_path = get_system_path_max() + needed_pad_len = system_max_path - path_len + computed_padding = _get_padding_string(needed_pad_len) + return padding_regex.sub(computed_padding, path) + return path + + def canonicalize_path(path): """Same as substitute_path_variables, but also take absolute path.""" path = substitute_path_variables(path) path = os.path.abspath(path) + path = _add_computed_padding(path) + return path -- cgit v1.2.3-60-g2f50