def _write_wrap_exe(self, wrapexec, wrappath, shebang=None, args=None, cwd=None): args = ' '.join(args) + ' "$@"' if args else '"$@"' cwd = 'cd {}'.format(cwd) if cwd else '' assembled_env = common.assemble_env().replace(self._snap_dir, '$SNAP') replace_path = r'{}/[a-z0-9][a-z0-9+-]*/install'.format( self._parts_dir) assembled_env = re.sub(replace_path, '$SNAP', assembled_env) executable = '"{}"'.format(wrapexec) if shebang is not None: new_shebang = re.sub(replace_path, '$SNAP', shebang) if new_shebang != shebang: # If the shebang was pointing to and executable within the # local 'parts' dir, have the wrapper script execute it # directly, since we can't use $SNAP in the shebang itself. executable = '"{}" "{}"'.format(new_shebang, wrapexec) script = ('#!/bin/sh\n' + '{}\n'.format(assembled_env) + '{}\n'.format(cwd) + 'LD_LIBRARY_PATH=$SNAP_LIBRARY_PATH:$LD_LIBRARY_PATH\n' 'exec {} {}\n'.format(executable, args)) with open(wrappath, 'w+') as f: f.write(script) os.chmod(wrappath, 0o755)
def _write_wrap_exe(self, wrapexec, wrappath, shebang=None, args=None, cwd=None): args = ' '.join(args) + ' "$@"' if args else '"$@"' cwd = 'cd {}'.format(cwd) if cwd else '' # If we are dealing with classic confinement it means all our # binaries are linked with `nodefaultlib` so this is harmless. # We do however want to be on the safe side and make sure no # ABI breakage happens by accidentally loading a library from # the classic system. include_library_paths = self._config_data['confinement'] != 'classic' assembled_env = common.assemble_env(include_library_paths) assembled_env = assembled_env.replace(self._snap_dir, '$SNAP') replace_path = r'{}/[a-z0-9][a-z0-9+-]*/install'.format( self._parts_dir) assembled_env = re.sub(replace_path, '$SNAP', assembled_env) executable = '"{}"'.format(wrapexec) if shebang is not None: new_shebang = re.sub(replace_path, '$SNAP', shebang) if new_shebang != shebang: # If the shebang was pointing to and executable within the # local 'parts' dir, have the wrapper script execute it # directly, since we can't use $SNAP in the shebang itself. executable = '"{}" "{}"'.format(new_shebang, wrapexec) script = ('#!/bin/sh\n' + '{}\n'.format(assembled_env) + '{}\n'.format(cwd) + 'LD_LIBRARY_PATH=$SNAP_LIBRARY_PATH:$LD_LIBRARY_PATH\n' 'exec {} {}\n'.format(executable, args)) with open(wrappath, 'w+') as f: f.write(script) os.chmod(wrappath, 0o755)
def _assemble_runtime_environment(self) -> str: # Classic confinement or building on a host that does not match the target base # means we cannot setup an environment that will work. if self._config_data["confinement"] == "classic": # Temporary workaround for snapd bug not expanding PATH: # We generate an empty runner which addresses the issue. # https://bugs.launchpad.net/snapd/+bug/1860369 return "" env = list() if self._project_config.project._snap_meta.base in ("core", "core16", "core18"): common.env = self._project_config.snap_env() assembled_env = common.assemble_env() common.reset_env() assembled_env = assembled_env.replace(self._prime_dir, "$SNAP") env.append(self._install_path_pattern.sub("$SNAP", assembled_env)) else: # TODO use something local to the meta package and # only add paths for directory items that actually exist. runtime_env = project_loader.runtime_env( self._prime_dir, self._project_config.project.arch_triplet ) for e in runtime_env: env.append(re.sub(self._prime_dir, "$SNAP", e)) env.append("export LD_LIBRARY_PATH=$SNAP_LIBRARY_PATH:$LD_LIBRARY_PATH") return "\n".join(env)
def _run_scriptlet(self, scriptlet_name: str, scriptlet: str, workdir: str) -> None: with tempfile.TemporaryDirectory() as tempdir: call_fifo = _NonBlockingRWFifo( os.path.join(tempdir, 'function_call')) feedback_fifo = _NonBlockingRWFifo( os.path.join(tempdir, 'call_feedback')) env = '' if common.is_snap(): # Since the snap is classic, $SNAP/bin is not on the $PATH. # Let's set an alias to make sure it's found (but only if it # exists). snapcraftctl_path = os.path.join(os.getenv('SNAP'), 'bin', 'snapcraftctl') if os.path.exists(snapcraftctl_path): env += 'alias snapcraftctl="$SNAP/bin/snapcraftctl"\n' env += common.assemble_env() # snapcraftctl only works consistently if it's using the exact same # interpreter as that used by snapcraft itself, thus the definition # of SNAPCRAFT_INTERPRETER. script = textwrap.dedent("""\ export SNAPCRAFTCTL_CALL_FIFO={call_fifo} export SNAPCRAFTCTL_FEEDBACK_FIFO={feedback_fifo} export SNAPCRAFT_INTERPRETER={interpreter} {env} {scriptlet} """.format(interpreter=sys.executable, call_fifo=call_fifo.path, feedback_fifo=feedback_fifo.path, env=env, scriptlet=scriptlet)) process = subprocess.Popen(['/bin/sh', '-e', '-c', script], cwd=workdir) status = None try: while status is None: function_call = call_fifo.read() if function_call: # Handle the function and let caller know that function # call has been handled (must contain at least a # newline, anything beyond is considered an error by # snapcraftctl) feedback_fifo.write('{}\n'.format( self._handle_builtin_function( scriptlet_name, function_call.strip()))) status = process.poll() # Don't loop TOO busily time.sleep(0.1) finally: call_fifo.close() feedback_fifo.close() if status: raise errors.ScriptletRunError(scriptlet_name=scriptlet_name, code=status)
def _generate_command_chain(self) -> List[str]: if self._command_chain is not None: return self._command_chain command_chain = list() # Classic confinement or building on a host that does not match the target base # means we cannot setup an environment that will work. if (self._config_data["confinement"] == "classic" or not self._is_host_compatible_with_base): assembled_env = None else: meta_runner = os.path.join(self._prime_dir, "snap", "command-chain", "snapcraft-runner") assembled_env = common.assemble_env() assembled_env = assembled_env.replace(self._prime_dir, "$SNAP") assembled_env = self._install_path_pattern.sub( "$SNAP", assembled_env) if assembled_env: os.makedirs(os.path.dirname(meta_runner), exist_ok=True) with open(meta_runner, "w") as f: print("#!/bin/sh", file=f) print(assembled_env, file=f) print( "export LD_LIBRARY_PATH=$SNAP_LIBRARY_PATH:$LD_LIBRARY_PATH", file=f, ) print('exec "$@"', file=f) os.chmod(meta_runner, 0o755) command_chain.append(os.path.relpath(meta_runner, self._prime_dir)) self._command_chain = command_chain return command_chain
def _generate_snapcraft_runner(self) -> Optional[str]: """Create runner if required. Return path relative to prime directory, if created.""" # Classic confinement or building on a host that does not match the target base # means we cannot setup an environment that will work. if (self._config_data["confinement"] == "classic" or not self._is_host_compatible_with_base or not self._snap_meta.apps): return None meta_runner = os.path.join(self._prime_dir, "snap", "command-chain", "snapcraft-runner") common.env = self._project_config.snap_env() assembled_env = common.assemble_env() assembled_env = assembled_env.replace(self._prime_dir, "$SNAP") assembled_env = self._install_path_pattern.sub("$SNAP", assembled_env) if assembled_env: os.makedirs(os.path.dirname(meta_runner), exist_ok=True) with open(meta_runner, "w") as f: print("#!/bin/sh", file=f) print(assembled_env, file=f) print( "export LD_LIBRARY_PATH=$SNAP_LIBRARY_PATH:$LD_LIBRARY_PATH", file=f) print('exec "$@"', file=f) os.chmod(meta_runner, 0o755) common.reset_env() return os.path.relpath(meta_runner, self._prime_dir)
def _get_installed_node_packages(self, cwd): # There is no yarn ls cmd = [os.path.join(self._npm_dir, "bin", "npm"), "ls", "--json"] try: full_cmd = [ '/bin/sh', '-c', ' '.join( list( map(lambda s: s.replace('export ', ''), common.assemble_env().split('\n'))) + cmd) ] output = subprocess.check_output(full_cmd, cwd=cwd) except subprocess.CalledProcessError as error: # XXX When dependencies have missing dependencies, an error like # this is printed to stderr: # npm ERR! peer dep missing: glob@*, required by [email protected] # retcode is not 0, which raises an exception. output = error.output.decode(sys.getfilesystemencoding()).strip() packages = collections.OrderedDict() output_json = json.loads(output, object_pairs_hook=collections.OrderedDict) dependencies = output_json.get("dependencies", []) while dependencies: key, value = dependencies.popitem(last=False) # XXX Just as above, dependencies without version are the ones # missing. if "version" in value: packages[key] = value["version"] if "dependencies" in value: dependencies.update(value["dependencies"]) return packages
def _write_wrap_exe(self, wrapexec, wrappath, shebang=None, args=None, cwd=None): if args: quoted_args = ['"{}"'.format(arg) for arg in args] else: quoted_args = [] args = ' '.join(quoted_args) + ' "$@"' if args else '"$@"' cwd = 'cd {}'.format(cwd) if cwd else '' # If we are dealing with classic confinement it means all our # binaries are linked with `nodefaultlib` but we still do # not want to leak PATH or other environment variables # that would affect the applications view of the classic # environment it is dropped into. replace_path = re.compile(r'{}/[a-z0-9][a-z0-9+-]*/install'.format( re.escape(self._parts_dir))) if self._config_data['confinement'] == 'classic': assembled_env = None else: assembled_env = common.assemble_env() assembled_env = assembled_env.replace(self._prime_dir, '$SNAP') assembled_env = replace_path.sub('$SNAP', assembled_env) executable = '"{}"'.format(wrapexec) if shebang: if shebang.startswith('/usr/bin/env '): shebang = shell_utils.which(shebang.split()[1]) new_shebang = replace_path.sub('$SNAP', shebang) new_shebang = re.sub(self._prime_dir, '$SNAP', new_shebang) if new_shebang != shebang: # If the shebang was pointing to and executable within the # local 'parts' dir, have the wrapper script execute it # directly, since we can't use $SNAP in the shebang itself. executable = '"{}" "{}"'.format(new_shebang, wrapexec) with open(wrappath, 'w+') as f: print('#!/bin/sh', file=f) if assembled_env: print('{}'.format(assembled_env), file=f) print( 'export LD_LIBRARY_PATH=$SNAP_LIBRARY_PATH:' '$LD_LIBRARY_PATH', file=f) if cwd: print('{}'.format(cwd), file=f) # TODO remove this once LP: #1656340 is fixed in snapd. print(dedent("""\ # Workaround for LP: #1656340 [ -n "$XDG_RUNTIME_DIR" ] && mkdir -p $XDG_RUNTIME_DIR -m 700 """), file=f) print('exec {} {}'.format(executable, args), file=f) os.chmod(wrappath, 0o755)
def _write_wrap_exe(self, wrapexec, wrappath, shebang=None, args=None, cwd=None): if args: quoted_args = ['"{}"'.format(arg) for arg in args] else: quoted_args = [] args = ' '.join(quoted_args) + ' "$@"' if args else '"$@"' cwd = 'cd {}'.format(cwd) if cwd else '' # If we are dealing with classic confinement it means all our # binaries are linked with `nodefaultlib` but we still do # not want to leak PATH or other environment variables # that would affect the applications view of the classic # environment it is dropped into. replace_path = re.compile(r'{}/[a-z0-9][a-z0-9+-]*/install'.format( re.escape(self._parts_dir))) # Confinement classic or when building on a host that does not match # the target base means we cannot setup an environment that will work. if (self._config_data['confinement'] == 'classic' or not self._is_host_compatible_with_base): assembled_env = None else: assembled_env = common.assemble_env() assembled_env = assembled_env.replace(self._prime_dir, '$SNAP') assembled_env = replace_path.sub('$SNAP', assembled_env) executable = '"{}"'.format(wrapexec) if shebang: if shebang.startswith('/usr/bin/env '): shebang = shell_utils.which(shebang.split()[1]) new_shebang = replace_path.sub('$SNAP', shebang) new_shebang = re.sub(self._prime_dir, '$SNAP', new_shebang) if new_shebang != shebang: # If the shebang was pointing to and executable within the # local 'parts' dir, have the wrapper script execute it # directly, since we can't use $SNAP in the shebang itself. executable = '"{}" "{}"'.format(new_shebang, wrapexec) with open(wrappath, 'w+') as f: print('#!/bin/sh', file=f) if assembled_env: print('{}'.format(assembled_env), file=f) print( 'export LD_LIBRARY_PATH=$SNAP_LIBRARY_PATH:' '$LD_LIBRARY_PATH', file=f) if cwd: print('{}'.format(cwd), file=f) print('exec {} {}'.format(executable, args), file=f) os.chmod(wrappath, 0o755)
def _get_env(): env = "" if common.is_snap(): # Since the snap is classic, there is no $PATH pointing into the snap, which # means snapcraftctl won't be found. We can't use aliases since they don't # persist into subshells. However, we know that snapcraftctl lives in its own # directory, so adding that to the PATH should have no ill side effects. env += 'export PATH="$PATH:$SNAP/bin/scriptlet-bin"\n' env += common.assemble_env() return env
def _get_env(): env = "" if common.is_snap(): # Since the snap is classic, $SNAP/bin is not on the $PATH. # Let's set an alias to make sure it's found (but only if it # exists). snapcraftctl_path = os.path.join(os.getenv("SNAP"), "bin", "snapcraftctl") if os.path.exists(snapcraftctl_path): env += 'alias snapcraftctl="$SNAP/bin/snapcraftctl"\n' env += common.assemble_env() return env
def _write_wrap_exe(self, wrapexec, wrappath, shebang=None, args=None, cwd=None): if args: quoted_args = ['"{}"'.format(arg) for arg in args] else: quoted_args = [] args = " ".join(quoted_args) + ' "$@"' if args else '"$@"' cwd = "cd {}".format(cwd) if cwd else "" # If we are dealing with classic confinement it means all our # binaries are linked with `nodefaultlib` but we still do # not want to leak PATH or other environment variables # that would affect the applications view of the classic # environment it is dropped into. replace_path = re.compile( r"{}/[a-z0-9][a-z0-9+-]*/install".format(re.escape(self._parts_dir)) ) # Confinement classic or when building on a host that does not match # the target base means we cannot setup an environment that will work. if ( self._config_data["confinement"] == "classic" or not self._is_host_compatible_with_base ): assembled_env = None else: assembled_env = common.assemble_env() assembled_env = assembled_env.replace(self._prime_dir, "$SNAP") assembled_env = replace_path.sub("$SNAP", assembled_env) executable = '"{}"'.format(wrapexec) if shebang: if shebang.startswith("/usr/bin/env "): shebang = shell_utils.which(shebang.split()[1]) new_shebang = replace_path.sub("$SNAP", shebang) new_shebang = re.sub(self._prime_dir, "$SNAP", new_shebang) if new_shebang != shebang: # If the shebang was pointing to and executable within the # local 'parts' dir, have the wrapper script execute it # directly, since we can't use $SNAP in the shebang itself. executable = '"{}" "{}"'.format(new_shebang, wrapexec) with open(wrappath, "w+") as f: print("#!/bin/sh", file=f) if assembled_env: print("{}".format(assembled_env), file=f) print( "export LD_LIBRARY_PATH=$SNAP_LIBRARY_PATH:$LD_LIBRARY_PATH", file=f ) if cwd: print("{}".format(cwd), file=f) print("exec {} {}".format(executable, args), file=f) os.chmod(wrappath, 0o755)
def _find_bin(binary, basedir): # If it doesn't exist it might be in the path logger.debug('Checking that {!r} is in the $PATH'.format(binary)) script = ('#!/bin/sh\n' + '{}\n'.format(common.assemble_env()) + 'which "{}"\n'.format(binary)) with tempfile.NamedTemporaryFile('w+') as tempf: tempf.write(script) tempf.flush() try: common.run(['/bin/sh', tempf.name], cwd=basedir, stdout=subprocess.DEVNULL) except subprocess.CalledProcessError: raise CommandError(binary)
def _assemble_runtime_environment(self) -> str: # Classic confinement or building on a host that does not match the target base # means we cannot setup an environment that will work. if self._config_data["confinement"] == "classic": # Temporary workaround for snapd bug not expanding PATH: # We generate an empty runner which addresses the issue. # https://bugs.launchpad.net/snapd/+bug/1860369 return "" common.env = self._project_config.snap_env() assembled_env = common.assemble_env() common.reset_env() assembled_env = assembled_env.replace(self._prime_dir, "$SNAP") assembled_env = self._install_path_pattern.sub("$SNAP", assembled_env) ld_library_env = "export LD_LIBRARY_PATH=$SNAP_LIBRARY_PATH:$LD_LIBRARY_PATH" return "\n".join([assembled_env, ld_library_env])
def _write_wrap_exe(self, wrapexec, wrappath, shebang=None, args=None, cwd=None): args = ' '.join(args) + ' "$@"' if args else '"$@"' cwd = 'cd {}'.format(cwd) if cwd else '' # If we are dealing with classic confinement it means all our # binaries are linked with `nodefaultlib` but we still do # not want to leak PATH or other environment variables # that would affect the applications view of the classic # environment it is dropped into. replace_path = re.compile(r'{}/[a-z0-9][a-z0-9+-]*/install'.format( re.escape(self._parts_dir))) if self._config_data['confinement'] == 'classic': assembled_env = None else: assembled_env = common.assemble_env() assembled_env = assembled_env.replace(self._prime_dir, '$SNAP') assembled_env = replace_path.sub('$SNAP', assembled_env) executable = '"{}"'.format(wrapexec) if shebang: if shebang.startswith('/usr/bin/env '): shebang = shell_utils.which(shebang.split()[1]) new_shebang = replace_path.sub('$SNAP', shebang) new_shebang = re.sub(self._prime_dir, '$SNAP', new_shebang) if new_shebang != shebang: # If the shebang was pointing to and executable within the # local 'parts' dir, have the wrapper script execute it # directly, since we can't use $SNAP in the shebang itself. executable = '"{}" "{}"'.format(new_shebang, wrapexec) with open(wrappath, 'w+') as f: print('#!/bin/sh', file=f) if assembled_env: print('{}'.format(assembled_env), file=f) print('export LD_LIBRARY_PATH=$SNAP_LIBRARY_PATH:' '$LD_LIBRARY_PATH', file=f) if cwd: print('{}'.format(cwd), file=f) print('exec {} {}'.format(executable, args), file=f) os.chmod(wrappath, 0o755)
def _assemble_runtime_environment(self) -> str: # Classic confinement or building on a host that does not match the target base # means we cannot setup an environment that will work. if self._config_data["confinement"] == "classic": # Temporary workaround for snapd bug not expanding PATH: # We generate an empty runner which addresses the issue. # https://bugs.launchpad.net/snapd/+bug/1860369 return "" env = list() if self._project_config.project._snap_meta.base == "core18": common.env = self._project_config.snap_env() assembled_env = common.assemble_env() common.reset_env() assembled_env = assembled_env.replace(self._prime_dir, "$SNAP") env.append(self._install_path_pattern.sub("$SNAP", assembled_env)) else: # TODO use something local to the meta package and # only add paths for directory items that actually exist. runtime_env = project_loader.runtime_env( self._prime_dir, self._project_config.project.arch_triplet ) for e in runtime_env: env.append(re.sub(self._prime_dir, "$SNAP", e)) if all( [ part._build_attributes.enable_patchelf() for part in self._project_config.all_parts ] ): # All ELF files have had rpath and interpreter patched. Strip all LD_LIBRARY_PATH variables env = [e for e in env if not e.startswith("export LD_LIBRARY_PATH=")] else: env.append( 'export LD_LIBRARY_PATH="$SNAP_LIBRARY_PATH${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"' ) return "\n".join(env)
def _write_wrap_exe(self, wrapexec, wrappath, shebang=None, args=None, cwd=None): if args: quoted_args = ['"{}"'.format(arg) for arg in args] else: quoted_args = [] args = " ".join(quoted_args) + ' "$@"' if args else '"$@"' cwd = "cd {}".format(cwd) if cwd else "" # If we are dealing with classic confinement it means all our # binaries are linked with `nodefaultlib` but we still do # not want to leak PATH or other environment variables # that would affect the applications view of the classic # environment it is dropped into. replace_path = re.compile(r"{}/[a-z0-9][a-z0-9+-]*/install".format( re.escape(self._parts_dir))) # Confinement classic or when building on a host that does not match # the target base means we cannot setup an environment that will work. if (self._config_data["confinement"] == "classic" or not self._is_host_compatible_with_base): assembled_env = None else: assembled_env = common.assemble_env() assembled_env = assembled_env.replace(self._prime_dir, "$SNAP") assembled_env = replace_path.sub("$SNAP", assembled_env) executable = '"{}"'.format(wrapexec) if shebang: if shebang.startswith("/usr/bin/env "): shebang = shell_utils.which(shebang.split()[1]) new_shebang = replace_path.sub("$SNAP", shebang) new_shebang = re.sub(self._prime_dir, "$SNAP", new_shebang) if new_shebang != shebang: # If the shebang was pointing to and executable within the # local 'parts' dir, have the wrapper script execute it # directly, since we can't use $SNAP in the shebang itself. executable = '"{}" "{}"'.format(new_shebang, wrapexec) with open(wrappath, "w+") as f: print("#!/bin/sh", file=f) if assembled_env: print("{}".format(assembled_env), file=f) print( 'export LD_LIBRARY_PATH="$SNAP_LIBRARY_PATH${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"', file=f, ) print( 'echo $LD_LIBRARY_PATH | grep -qE "::|^:|:$" && ' 'echo "WARNING: an empty LD_LIBRARY_PATH has been set. ' "CWD will be added to the library path. " 'This can cause the incorrect library to be loaded."', file=f, ) if cwd: print("{}".format(cwd), file=f) print("exec {} {}".format(executable, args), file=f) os.chmod(wrappath, 0o755)