import tempfile
import zipfile
import hashlib
+import pathlib
-__all__ = ["BuildPlan", "BuildProducts", "LocalBuildProducts"]
+__all__ = ["BuildPlan", "BuildProducts", "LocalBuildProducts", "RemoteSSHBuildProducts"]
+
class BuildPlan:
os.chdir(root)
for filename, content in self.files.items():
- filename = os.path.normpath(filename)
- # Just to make sure we don't accidentally overwrite anything outside of build root.
- assert not filename.startswith("..")
+ filename = pathlib.Path(filename)
+ # Forbid parent directory components completely to avoid the possibility
+ # of writing outside the build root.
+ assert ".." not in filename.parts
dirname = os.path.dirname(filename)
if dirname:
os.makedirs(dirname, exist_ok=True)
finally:
os.chdir(cwd)
+ def execute_remote_ssh(self, *, connect_to = {}, root, run_script=True):
+ """
+ Execute build plan using the remote SSH strategy. Files from the build
+ plan are transferred via SFTP to the directory ``root`` on a remote
+ server. If ``run_script`` is ``True``, the ``paramiko`` SSH client will
+ then run ``{script}.sh``. ``root`` can either be an absolute or
+ relative (to the login directory) path.
+
+ ``connect_to`` is a dictionary that holds all input arguments to
+ ``paramiko``'s ``SSHClient.connect``
+ (`documentation <http://docs.paramiko.org/en/stable/api/client.html#paramiko.client.SSHClient.connect>`_).
+ At a minimum, the ``hostname`` input argument must be supplied in this
+ dictionary as the remote server.
+
+ Returns :class:`RemoteSSHBuildProducts`.
+ """
+ from paramiko import SSHClient
+
+ with SSHClient() as client:
+ client.load_system_host_keys()
+ client.connect(**connect_to)
+
+ with client.open_sftp() as sftp:
+ def mkdir_exist_ok(path):
+ try:
+ sftp.mkdir(str(path))
+ except IOError as e:
+ # mkdir fails if directory exists. This is fine in nmigen.build.
+ # Reraise errors containing e.errno info.
+ if e.errno:
+ raise e
+
+ def mkdirs(path):
+ # Iteratively create parent directories of a file by iterating over all
+ # parents except for the root ("."). Slicing the parents results in
+ # TypeError, so skip over the root ("."); this also handles files
+ # already in the root directory.
+ for parent in reversed(path.parents):
+ if parent == pathlib.PurePosixPath("."):
+ continue
+ else:
+ mkdir_exist_ok(parent)
+
+ mkdir_exist_ok(root)
+
+ sftp.chdir(root)
+ for filename, content in self.files.items():
+ filename = pathlib.PurePosixPath(filename)
+ assert ".." not in filename.parts
+
+ mkdirs(filename)
+
+ mode = "wt" if isinstance(content, str) else "wb"
+ with sftp.file(str(filename), mode) as f:
+ # "b/t" modifier ignored in SFTP.
+ if mode == "wt":
+ f.write(content.encode("utf-8"))
+ else:
+ f.write(content)
+
+ if run_script:
+ transport = client.get_transport()
+ channel = transport.open_session()
+ channel.set_combine_stderr(True)
+
+ cmd = "if [ -f ~/.profile ]; then . ~/.profile; fi && cd {} && sh {}.sh".format(root, self.script)
+ channel.exec_command(cmd)
+
+ # Show the output from the server while products are built.
+ buf = channel.recv(1024)
+ while buf:
+ print(buf.decode("utf-8"), end="")
+ buf = channel.recv(1024)
+
+ return RemoteSSHBuildProducts(connect_to, root)
+
def execute(self):
"""
Execute build plan using the default strategy. Use one of the ``execute_*`` methods
super().get(filename, mode)
with open(os.path.join(self.__root, filename), "r" + mode) as f:
return f.read()
+
+
+class RemoteSSHBuildProducts(BuildProducts):
+ def __init__(self, connect_to, root):
+ self.__connect_to = connect_to
+ self.__root = root
+
+ def get(self, filename, mode="b"):
+ super().get(filename, mode)
+
+ from paramiko import SSHClient
+
+ with SSHClient() as client:
+ client.load_system_host_keys()
+ client.connect(**self.__connect_to)
+
+ with client.open_sftp() as sftp:
+ sftp.chdir(self.__root)
+
+ with sftp.file(filename, "r" + mode) as f:
+ # "b/t" modifier ignored in SFTP.
+ if mode == "t":
+ return f.read().decode("utf-8")
+ else:
+ return f.read()