Martin Blanchard pushed to branch mablanch/61-bazel-support at BuildGrid / buildgrid
Commits:
- 
77c066d4
by Martin Blanchard at 2018-09-06T09:44:00Z
- 
03c61990
by Martin Blanchard at 2018-09-06T14:16:14Z
- 
f12ec81d
by Martin Blanchard at 2018-09-06T14:28:35Z
- 
0a165ab5
by Martin Blanchard at 2018-09-06T14:28:35Z
- 
b993502a
by Martin Blanchard at 2018-09-06T15:11:46Z
- 
a293b484
by Martin Blanchard at 2018-09-06T15:11:48Z
- 
215b849a
by Martin Blanchard at 2018-09-06T15:11:48Z
- 
fb92e1a2
by Martin Blanchard at 2018-09-06T15:11:48Z
7 changed files:
- .coveragerc
- .gitlab-ci.yml
- README.rst
- buildgrid/_app/bots/temp_directory.py
- buildgrid/server/execution/execution_service.py
- buildgrid/utils.py
- setup.py
Changes:
| 1 | 1 |  [run]
 | 
| 2 | 2 |  concurrency = multiprocessing
 | 
| 3 | +data_file = .coverage
 | |
| 3 | 4 |  include =
 | 
| 4 | 5 |    */buildgrid/*
 | 
| 5 | 6 |  | 
| ... | ... | @@ -7,11 +7,13 @@ variables: | 
| 7 | 7 |  stages:
 | 
| 8 | 8 |    - test
 | 
| 9 | 9 |    - post
 | 
| 10 | +  - deploy
 | |
| 11 | + | |
| 10 | 12 |  | 
| 11 | 13 |  before_script:
 | 
| 12 | -  - pip3 install setuptools
 | |
| 14 | +  - python3 -m pip install --upgrade setuptools pip
 | |
| 13 | 15 |    - export PATH=~/.local/bin:${PATH}
 | 
| 14 | -  - pip3 install --user -e .
 | |
| 16 | +  - python3 -m pip install --user --editable ".[tests]"
 | |
| 15 | 17 |  | 
| 16 | 18 |  .tests-template: &linux-tests
 | 
| 17 | 19 |    stage: test
 | 
| ... | ... | @@ -20,7 +22,7 @@ before_script: | 
| 20 | 22 |    script:
 | 
| 21 | 23 |      - python3 setup.py test
 | 
| 22 | 24 |      - mkdir -p coverage/
 | 
| 23 | -    - cp .coverage.* coverage/coverage."${CI_JOB_NAME}"
 | |
| 25 | +    - cp .coverage coverage/coverage."${CI_JOB_NAME}"
 | |
| 24 | 26 |    artifacts:
 | 
| 25 | 27 |      paths:
 | 
| 26 | 28 |      - coverage/
 | 
| ... | ... | @@ -33,6 +35,9 @@ before_script: | 
| 33 | 35 |      - ${BGD} bot dummy &
 | 
| 34 | 36 |      - ${BGD} execute request-dummy --wait-for-completion
 | 
| 35 | 37 |  | 
| 38 | + | |
| 39 | +# Test stage, build and test the code.
 | |
| 40 | +#
 | |
| 36 | 41 |  tests-debian-stretch:
 | 
| 37 | 42 |    <<: *linux-tests
 | 
| 38 | 43 |  | 
| ... | ... | @@ -40,10 +45,13 @@ run-dummy-job-debian: | 
| 40 | 45 |    image: buildstream/buildstream-debian
 | 
| 41 | 46 |    <<: *dummy-job
 | 
| 42 | 47 |  | 
| 43 | -build-docs:
 | |
| 44 | -  stage: test
 | |
| 48 | + | |
| 49 | +# Post-build stage, documentation, coverage report...
 | |
| 50 | +#
 | |
| 51 | +documentation:
 | |
| 52 | +  stage: post
 | |
| 45 | 53 |    script:
 | 
| 46 | -    - pip3 install --editable ".[docs]"
 | |
| 54 | +    - python3 -m pip install --user --editable ".[docs]"
 | |
| 47 | 55 |      - make -C docs html
 | 
| 48 | 56 |      - mkdir -p documentation/
 | 
| 49 | 57 |      - cp -a docs/build/html/. documentation/
 | 
| ... | ... | @@ -51,31 +59,36 @@ build-docs: | 
| 51 | 59 |      paths:
 | 
| 52 | 60 |      - documentation/
 | 
| 53 | 61 |  | 
| 54 | - | |
| 55 | 62 |  coverage:
 | 
| 56 | 63 |    stage: post
 | 
| 57 | -  coverage: '/TOTAL +\d+ +\d+ +(\d+\.\d+)%/'
 | |
| 58 | -  script:
 | |
| 59 | -    - pip3 install coverage==4.4.0
 | |
| 60 | -    - mkdir report
 | |
| 61 | -    - cd report
 | |
| 62 | -    - cp ../coverage/coverage.* .
 | |
| 63 | -    - ls coverage.*
 | |
| 64 | -    - coverage combine --rcfile=../.coveragerc -a coverage.*
 | |
| 65 | -    - coverage report --rcfile=../.coveragerc -m
 | |
| 66 | 64 |    dependencies:
 | 
| 67 | 65 |    - tests-debian-stretch
 | 
| 66 | +  coverage: '/TOTAL +\d+ +\d+ +(\d+\.\d+)%/'
 | |
| 67 | +  script:
 | |
| 68 | +    - python3 -m pip install --user --editable ".[tests]"
 | |
| 69 | +    - cd coverage/
 | |
| 70 | +    - ls -l .
 | |
| 71 | +    - python3 -m coverage combine --rcfile=../.coveragerc --append coverage.*
 | |
| 72 | +    - python3 -m coverage html --rcfile=../.coveragerc --directory .
 | |
| 73 | +    - python3 -m coverage report --rcfile=../.coveragerc --show-missing
 | |
| 74 | +    - python3 -m coverage erase --rcfile=../.coveragerc
 | |
| 75 | +  artifacts:
 | |
| 76 | +    paths:
 | |
| 77 | +    - coverage/
 | |
| 68 | 78 |  | 
| 69 | -# Deploy, only for merges which land on master branch.
 | |
| 79 | +# Deployement stage, only for merges which land on master branch.
 | |
| 70 | 80 |  #
 | 
| 71 | 81 |  pages:
 | 
| 72 | -  stage: post
 | |
| 82 | +  stage: deploy
 | |
| 73 | 83 |    dependencies:
 | 
| 74 | -  - tests-debian-stretch
 | |
| 75 | -  - build-docs
 | |
| 84 | +  - coverage
 | |
| 85 | +  - documentation
 | |
| 76 | 86 |    script:
 | 
| 77 | -  - cp -a coverage/. public/
 | |
| 87 | +  - mkdir -p public/coverage/
 | |
| 88 | +  - cp -a coverage/* public/coverage/
 | |
| 89 | +  - ls -la public/coverage/
 | |
| 78 | 90 |    - cp -a documentation/* public/
 | 
| 91 | +  - ls -la public/
 | |
| 79 | 92 |    artifacts:
 | 
| 80 | 93 |      paths:
 | 
| 81 | 94 |      - public/
 | 
| ... | ... | @@ -8,7 +8,7 @@ About | 
| 8 | 8 |     :target: https://gitlab.com/BuildStream/buildstream/commits/master
 | 
| 9 | 9 |  | 
| 10 | 10 |  .. image:: https://gitlab.com/BuildGrid/buildgrid/badges/master/coverage.svg?job=coverage
 | 
| 11 | -   :target: https://gitlab.com/BuildGrid/buildgrid/commits/master
 | |
| 11 | +   :target: https://buildgrid.gitlab.io/buildgrid/coverage
 | |
| 12 | 12 |  | 
| 13 | 13 |  BuildGrid is a Python remote execution service which implements Google's
 | 
| 14 | 14 |  `Remote Execution API`_ and the `Remote Workers API`_. The project's goal is to
 | 
| ... | ... | @@ -19,71 +19,100 @@ import tempfile | 
| 19 | 19 |  | 
| 20 | 20 |  from google.protobuf import any_pb2
 | 
| 21 | 21 |  | 
| 22 | -from buildgrid.utils import read_file, create_digest, write_fetch_directory, parse_to_pb2_from_fetch
 | |
| 23 | 22 |  from buildgrid._protos.build.bazel.remote.execution.v2 import remote_execution_pb2, remote_execution_pb2_grpc
 | 
| 24 | 23 |  from buildgrid._protos.google.bytestream import bytestream_pb2_grpc
 | 
| 24 | +from buildgrid.utils import write_fetch_directory, parse_to_pb2_from_fetch
 | |
| 25 | +from buildgrid.utils import output_file_maker, output_directory_maker
 | |
| 25 | 26 |  | 
| 26 | 27 |  | 
| 27 | 28 |  def work_temp_directory(context, lease):
 | 
| 28 | -    """ Bot downloads directories and files into a temp directory,
 | |
| 29 | -    then uploads results back to CAS
 | |
| 29 | +    """Executes a lease for a build action, using host tools.
 | |
| 30 | 30 |      """
 | 
| 31 | 31 |  | 
| 32 | -    parent = context.parent
 | |
| 33 | 32 |      stub_bytestream = bytestream_pb2_grpc.ByteStreamStub(context.cas_channel)
 | 
| 33 | +    instance_name = context.parent
 | |
| 34 | +    logger = context.logger
 | |
| 34 | 35 |  | 
| 35 | 36 |      action_digest = remote_execution_pb2.Digest()
 | 
| 36 | 37 |      lease.payload.Unpack(action_digest)
 | 
| 37 | 38 |  | 
| 38 | -    action = remote_execution_pb2.Action()
 | |
| 39 | +    action = parse_to_pb2_from_fetch(remote_execution_pb2.Action(),
 | |
| 40 | +                                     stub_bytestream, action_digest, instance_name)
 | |
| 39 | 41 |  | 
| 40 | -    action = parse_to_pb2_from_fetch(action, stub_bytestream, action_digest, parent)
 | |
| 42 | +    with tempfile.TemporaryDirectory() as temp_directory:
 | |
| 43 | +        command = parse_to_pb2_from_fetch(remote_execution_pb2.Command(),
 | |
| 44 | +                                          stub_bytestream, action.command_digest, instance_name)
 | |
| 41 | 45 |  | 
| 42 | -    with tempfile.TemporaryDirectory() as temp_dir:
 | |
| 46 | +        write_fetch_directory(temp_directory, stub_bytestream,
 | |
| 47 | +                              action.input_root_digest, instance_name)
 | |
| 43 | 48 |  | 
| 44 | -        command = remote_execution_pb2.Command()
 | |
| 45 | -        command = parse_to_pb2_from_fetch(command, stub_bytestream, action.command_digest, parent)
 | |
| 46 | - | |
| 47 | -        arguments = "cd {} &&".format(temp_dir)
 | |
| 49 | +        environment = os.environ.copy()
 | |
| 50 | +        for variable in command.environment_variables:
 | |
| 51 | +            if variable.name not in ['PATH', 'PWD']:
 | |
| 52 | +                environment[variable.name] = variable.value
 | |
| 48 | 53 |  | 
| 54 | +        command_line = list()
 | |
| 49 | 55 |          for argument in command.arguments:
 | 
| 50 | -            arguments += " {}".format(argument)
 | |
| 51 | - | |
| 52 | -        context.logger.info(arguments)
 | |
| 53 | - | |
| 54 | -        write_fetch_directory(temp_dir, stub_bytestream, action.input_root_digest, parent)
 | |
| 55 | - | |
| 56 | -        proc = subprocess.Popen(arguments,
 | |
| 57 | -                                shell=True,
 | |
| 58 | -                                stdin=subprocess.PIPE,
 | |
| 59 | -                                stdout=subprocess.PIPE)
 | |
| 60 | - | |
| 61 | -        # TODO: Should return the std_out to the user
 | |
| 62 | -        proc.communicate()
 | |
| 63 | - | |
| 64 | -        result = remote_execution_pb2.ActionResult()
 | |
| 65 | -        requests = []
 | |
| 66 | -        for output_file in command.output_files:
 | |
| 67 | -            path = os.path.join(temp_dir, output_file)
 | |
| 68 | -            chunk = read_file(path)
 | |
| 69 | - | |
| 70 | -            digest = create_digest(chunk)
 | |
| 71 | - | |
| 72 | -            result.output_files.extend([remote_execution_pb2.OutputFile(path=output_file,
 | |
| 73 | -                                                                        digest=digest)])
 | |
| 74 | - | |
| 75 | -            requests.append(remote_execution_pb2.BatchUpdateBlobsRequest.Request(
 | |
| 76 | -                digest=digest, data=chunk))
 | |
| 77 | - | |
| 78 | -        request = remote_execution_pb2.BatchUpdateBlobsRequest(instance_name=parent,
 | |
| 79 | -                                                               requests=requests)
 | |
| 56 | +            command_line.append(argument.strip())
 | |
| 57 | + | |
| 58 | +        working_directory = None
 | |
| 59 | +        if command.working_directory:
 | |
| 60 | +            working_directory = os.path.join(temp_directory,
 | |
| 61 | +                                             command.working_directory)
 | |
| 62 | +            os.makedirs(working_directory, exist_ok=True)
 | |
| 63 | +        else:
 | |
| 64 | +            working_directory = temp_directory
 | |
| 65 | + | |
| 66 | +        # Ensure that output files structure exists:
 | |
| 67 | +        for output_path in command.output_files:
 | |
| 68 | +            directory_path = os.path.join(working_directory,
 | |
| 69 | +                                          os.path.dirname(output_path))
 | |
| 70 | +            os.makedirs(directory_path, exist_ok=True)
 | |
| 71 | + | |
| 72 | +        logger.debug(' '.join(command_line))
 | |
| 73 | + | |
| 74 | +        process = subprocess.Popen(command_line,
 | |
| 75 | +                                   cwd=working_directory,
 | |
| 76 | +                                   universal_newlines=True,
 | |
| 77 | +                                   env=environment,
 | |
| 78 | +                                   stdin=subprocess.PIPE,
 | |
| 79 | +                                   stdout=subprocess.PIPE)
 | |
| 80 | +        # TODO: Should return the stdout and stderr in the ActionResult.
 | |
| 81 | +        process.communicate()
 | |
| 82 | + | |
| 83 | +        update_requests = remote_execution_pb2.BatchUpdateBlobsRequest(instance_name=instance_name)
 | |
| 84 | +        action_result = remote_execution_pb2.ActionResult()
 | |
| 85 | + | |
| 86 | +        for output_path in command.output_files:
 | |
| 87 | +            file_path = os.path.join(working_directory, output_path)
 | |
| 88 | +            # Missing outputs should simply be omitted in ActionResult:
 | |
| 89 | +            if not os.path.isfile(file_path):
 | |
| 90 | +                continue
 | |
| 91 | + | |
| 92 | +            # OutputFile.path should be relative to the working direcory:
 | |
| 93 | +            output_file, update_request = output_file_maker(file_path, working_directory)
 | |
| 94 | + | |
| 95 | +            action_result.output_files.extend([output_file])
 | |
| 96 | +            update_requests.requests.extend([update_request])
 | |
| 97 | + | |
| 98 | +        for output_path in command.output_directories:
 | |
| 99 | +            directory_path = os.path.join(working_directory, output_path)
 | |
| 100 | +            # Missing outputs should simply be omitted in ActionResult:
 | |
| 101 | +            if not os.path.isdir(directory_path):
 | |
| 102 | +                continue
 | |
| 103 | + | |
| 104 | +            # OutputDirectory.path should be relative to the working direcory:
 | |
| 105 | +            output_directory, update_request = output_directory_maker(directory_path, working_directory)
 | |
| 106 | + | |
| 107 | +            action_result.output_directories.extend([output_directory])
 | |
| 108 | +            update_requests.requests.extend(update_request)
 | |
| 80 | 109 |  | 
| 81 | 110 |          stub_cas = remote_execution_pb2_grpc.ContentAddressableStorageStub(context.cas_channel)
 | 
| 82 | -        stub_cas.BatchUpdateBlobs(request)
 | |
| 111 | +        stub_cas.BatchUpdateBlobs(update_requests)
 | |
| 83 | 112 |  | 
| 84 | -        result_any = any_pb2.Any()
 | |
| 85 | -        result_any.Pack(result)
 | |
| 113 | +        action_result_any = any_pb2.Any()
 | |
| 114 | +        action_result_any.Pack(action_result)
 | |
| 86 | 115 |  | 
| 87 | -        lease.result.CopyFrom(result_any)
 | |
| 116 | +        lease.result.CopyFrom(action_result_any)
 | |
| 88 | 117 |  | 
| 89 | 118 |      return lease | 
| ... | ... | @@ -86,6 +86,11 @@ class ExecutionService(remote_execution_pb2_grpc.ExecutionServicer): | 
| 86 | 86 |              yield operations_pb2.Operation()
 | 
| 87 | 87 |  | 
| 88 | 88 |      def _get_instance(self, name):
 | 
| 89 | +        # If client does not support multiple instances, it may omit the
 | |
| 90 | +        # instance name request parameter, so better map our default:
 | |
| 91 | +        if not name and len(self._instances) == 1:
 | |
| 92 | +            name = next(iter(self._instances))
 | |
| 93 | + | |
| 89 | 94 |          try:
 | 
| 90 | 95 |              return self._instances[name]
 | 
| 91 | 96 |  | 
| ... | ... | @@ -13,6 +13,7 @@ | 
| 13 | 13 |  # limitations under the License.
 | 
| 14 | 14 |  | 
| 15 | 15 |  | 
| 16 | +from operator import attrgetter
 | |
| 16 | 17 |  import os
 | 
| 17 | 18 |  | 
| 18 | 19 |  from buildgrid.settings import HASH
 | 
| ... | ... | @@ -31,30 +32,59 @@ def gen_fetch_blob(stub, digest, instance_name=""): | 
| 31 | 32 |          yield response.data
 | 
| 32 | 33 |  | 
| 33 | 34 |  | 
| 34 | -def write_fetch_directory(directory, stub, digest, instance_name=""):
 | |
| 35 | -    """ Given a directory digest, fetches files and writes them to a directory
 | |
| 35 | +def write_fetch_directory(root_directory, stub, digest, instance_name=None):
 | |
| 36 | +    """Locally replicates a directory from CAS.
 | |
| 37 | + | |
| 38 | +    Args:
 | |
| 39 | +        root_directory (str): local directory to populate.
 | |
| 40 | +        stub (): gRPC stub for CAS communication.
 | |
| 41 | +        digest (Digest): digest for the directory to fetch from CAS.
 | |
| 42 | +        instance_name (str, optional): farm instance name to query data from.
 | |
| 36 | 43 |      """
 | 
| 37 | -    # TODO: Extend to symlinks and inner directories
 | |
| 38 | -    # pathlib.Path('/my/directory').mkdir(parents=True, exist_ok=True)
 | |
| 44 | +    if not os.path.isabs(root_directory):
 | |
| 45 | +        root_directory = os.path.abspath(root_directory)
 | |
| 46 | +    if not os.path.exists(root_directory):
 | |
| 47 | +        os.makedirs(root_directory, exist_ok=True)
 | |
| 39 | 48 |  | 
| 40 | -    directory_pb2 = remote_execution_pb2.Directory()
 | |
| 41 | -    directory_pb2 = parse_to_pb2_from_fetch(directory_pb2, stub, digest, instance_name)
 | |
| 49 | +    directory = parse_to_pb2_from_fetch(remote_execution_pb2.Directory(),
 | |
| 50 | +                                        stub, digest, instance_name)
 | |
| 51 | + | |
| 52 | +    for directory_node in directory.directories:
 | |
| 53 | +        child_path = os.path.join(root_directory, directory_node.name)
 | |
| 54 | + | |
| 55 | +        write_fetch_directory(child_path, stub, directory_node.digest, instance_name)
 | |
| 56 | + | |
| 57 | +    for file_node in directory.files:
 | |
| 58 | +        child_path = os.path.join(root_directory, file_node.name)
 | |
| 42 | 59 |  | 
| 43 | -    for file_node in directory_pb2.files:
 | |
| 44 | -        path = os.path.join(directory, file_node.name)
 | |
| 45 | -        with open(path, 'wb') as f:
 | |
| 46 | -            write_fetch_blob(f, stub, file_node.digest, instance_name)
 | |
| 60 | +        with open(child_path, 'wb') as child_file:
 | |
| 61 | +            write_fetch_blob(child_file, stub, file_node.digest, instance_name)
 | |
| 47 | 62 |  | 
| 63 | +    for symlink_node in directory.symlinks:
 | |
| 64 | +        child_path = os.path.join(root_directory, symlink_node.name)
 | |
| 48 | 65 |  | 
| 49 | -def write_fetch_blob(out, stub, digest, instance_name=""):
 | |
| 50 | -    """ Given an output buffer, fetches blob and writes to buffer
 | |
| 66 | +        if os.path.isabs(symlink_node.target):
 | |
| 67 | +            continue  # No out of temp-directory links for now.
 | |
| 68 | +        target_path = os.path.join(root_directory, symlink_node.target)
 | |
| 69 | + | |
| 70 | +        os.symlink(child_path, target_path)
 | |
| 71 | + | |
| 72 | + | |
| 73 | +def write_fetch_blob(target_file, stub, digest, instance_name=None):
 | |
| 74 | +    """Extracts a blob from CAS into a local file.
 | |
| 75 | + | |
| 76 | +    Args:
 | |
| 77 | +        target_file (str): local file to write.
 | |
| 78 | +        stub (): gRPC stub for CAS communication.
 | |
| 79 | +        digest (Digest): digest for the blob to fetch from CAS.
 | |
| 80 | +        instance_name (str, optional): farm instance name to query data from.
 | |
| 51 | 81 |      """
 | 
| 52 | 82 |  | 
| 53 | 83 |      for stream in gen_fetch_blob(stub, digest, instance_name):
 | 
| 54 | -        out.write(stream)
 | |
| 84 | +        target_file.write(stream)
 | |
| 85 | +    target_file.flush()
 | |
| 55 | 86 |  | 
| 56 | -    out.flush()
 | |
| 57 | -    assert digest.size_bytes == os.fstat(out.fileno()).st_size
 | |
| 87 | +    assert digest.size_bytes == os.fstat(target_file.fileno()).st_size
 | |
| 58 | 88 |  | 
| 59 | 89 |  | 
| 60 | 90 |  def parse_to_pb2_from_fetch(pb2, stub, digest, instance_name=""):
 | 
| ... | ... | @@ -70,7 +100,15 @@ def parse_to_pb2_from_fetch(pb2, stub, digest, instance_name=""): | 
| 70 | 100 |  | 
| 71 | 101 |  | 
| 72 | 102 |  def create_digest(bytes_to_digest):
 | 
| 73 | -    """ Creates a hash based on the hex digest and returns the digest
 | |
| 103 | +    """Computes the :obj:`Digest` of a piece of data.
 | |
| 104 | + | |
| 105 | +    The :obj:`Digest` of a data is a function of its hash **and** size.
 | |
| 106 | + | |
| 107 | +    Args:
 | |
| 108 | +        bytes_to_digest (bytes): byte data to digest.
 | |
| 109 | + | |
| 110 | +    Returns:
 | |
| 111 | +        :obj:`Digest`: The gRPC :obj:`Digest` for the given byte data.
 | |
| 74 | 112 |      """
 | 
| 75 | 113 |      return remote_execution_pb2.Digest(hash=HASH(bytes_to_digest).hexdigest(),
 | 
| 76 | 114 |                                         size_bytes=len(bytes_to_digest))
 | 
| ... | ... | @@ -107,6 +145,206 @@ def file_maker(file_path, file_digest): | 
| 107 | 145 |                                           is_executable=os.access(file_path, os.X_OK))
 | 
| 108 | 146 |  | 
| 109 | 147 |  | 
| 110 | -def read_file(read):
 | |
| 111 | -    with open(read, 'rb') as f:
 | |
| 112 | -        return f.read() | |
| 148 | +def directory_maker(directory_path):
 | |
| 149 | +    """Creates a :obj:`Directory` from a local directory.
 | |
| 150 | + | |
| 151 | +    Args:
 | |
| 152 | +        directory_path (str): absolute or relative path to a local directory.
 | |
| 153 | + | |
| 154 | +    Returns:
 | |
| 155 | +        :obj:`Directory`, list of :obj:`Directory`, list of
 | |
| 156 | +        :obj:`BatchUpdateBlobsRequest`: Tuple of a new gRPC :obj:`Directory` for
 | |
| 157 | +        the directory pointed by `directory_path`, a list of new gRPC
 | |
| 158 | +        :obj:`Directory` for every children of that directory and the
 | |
| 159 | +        corresponding list of :obj:`BatchUpdateBlobsRequest` for CAS upload.
 | |
| 160 | + | |
| 161 | +        The :obj:`Directory` children list may come in any order.
 | |
| 162 | + | |
| 163 | +        The :obj:`BatchUpdateBlobsRequest` list may come in any order. However,
 | |
| 164 | +        its last element is guaranteed to be the root :obj:`Direcotry`'s
 | |
| 165 | +        request.
 | |
| 166 | +    """
 | |
| 167 | +    if not os.path.isabs(directory_path):
 | |
| 168 | +        directory_path = os.path.abspath(directory_path)
 | |
| 169 | + | |
| 170 | +    child_directories = list()
 | |
| 171 | +    update_requests = list()
 | |
| 172 | + | |
| 173 | +    files, directories, symlinks = list(), list(), list()
 | |
| 174 | +    for directory_entry in os.scandir(directory_path):
 | |
| 175 | +        # Create a FileNode and corresponding BatchUpdateBlobsRequest:
 | |
| 176 | +        if directory_entry.is_file(follow_symlinks=False):
 | |
| 177 | +            node_blob = read_file(directory_entry.path)
 | |
| 178 | +            node_digest = create_digest(node_blob)
 | |
| 179 | + | |
| 180 | +            node = remote_execution_pb2.FileNode()
 | |
| 181 | +            node.name = directory_entry.name
 | |
| 182 | +            node.digest.CopyFrom(node_digest)
 | |
| 183 | +            node.is_executable = os.access(directory_entry.path, os.X_OK)
 | |
| 184 | + | |
| 185 | +            node_request = remote_execution_pb2.BatchUpdateBlobsRequest.Request(digest=node_digest)
 | |
| 186 | +            node_request.data = node_blob
 | |
| 187 | + | |
| 188 | +            update_requests.append(node_request)
 | |
| 189 | +            files.append(node)
 | |
| 190 | + | |
| 191 | +        # Create a DirectoryNode and corresponding BatchUpdateBlobsRequest:
 | |
| 192 | +        elif directory_entry.is_dir(follow_symlinks=False):
 | |
| 193 | +            node_directory, node_children, node_requests = directory_maker(directory_entry.path)
 | |
| 194 | + | |
| 195 | +            node = remote_execution_pb2.DirectoryNode()
 | |
| 196 | +            node.name = directory_entry.name
 | |
| 197 | +            node.digest.CopyFrom(node_requests[-1].digest)
 | |
| 198 | + | |
| 199 | +            child_directories.extend(node_children)
 | |
| 200 | +            child_directories.append(node_directory)
 | |
| 201 | +            update_requests.extend(node_requests)
 | |
| 202 | +            directories.append(node)
 | |
| 203 | + | |
| 204 | +        # Create a SymlinkNode if necessary;
 | |
| 205 | +        elif os.path.islink(directory_entry.path):
 | |
| 206 | +            node_target = os.readlink(directory_entry.path)
 | |
| 207 | + | |
| 208 | +            node = remote_execution_pb2.SymlinkNode()
 | |
| 209 | +            node.name = directory_entry.name
 | |
| 210 | +            node.target = node_target
 | |
| 211 | + | |
| 212 | +            symlinks.append(node)
 | |
| 213 | + | |
| 214 | +    files.sort(key=attrgetter('name'))
 | |
| 215 | +    directories.sort(key=attrgetter('name'))
 | |
| 216 | +    symlinks.sort(key=attrgetter('name'))
 | |
| 217 | + | |
| 218 | +    directory = remote_execution_pb2.Directory()
 | |
| 219 | +    directory.files.extend(files)
 | |
| 220 | +    directory.directories.extend(directories)
 | |
| 221 | +    directory.symlinks.extend(symlinks)
 | |
| 222 | + | |
| 223 | +    directory_blob = directory.SerializeToString()
 | |
| 224 | +    directory_digest = create_digest(directory_blob)
 | |
| 225 | + | |
| 226 | +    update_request = remote_execution_pb2.BatchUpdateBlobsRequest.Request(digest=directory_digest)
 | |
| 227 | +    update_request.data = directory_blob
 | |
| 228 | + | |
| 229 | +    update_requests.append(update_request)
 | |
| 230 | + | |
| 231 | +    return directory, child_directories, update_requests
 | |
| 232 | + | |
| 233 | + | |
| 234 | +def read_file(file_path):
 | |
| 235 | +    """Loads raw file content in memory.
 | |
| 236 | + | |
| 237 | +    Args:
 | |
| 238 | +        file_path (str): path to the target file.
 | |
| 239 | + | |
| 240 | +    Returns:
 | |
| 241 | +        bytes: Raw file's content until EOF.
 | |
| 242 | + | |
| 243 | +    Raises:
 | |
| 244 | +        OSError: If `file_path` does not exist or is not readable.
 | |
| 245 | +    """
 | |
| 246 | +    with open(file_path, 'rb') as byte_file:
 | |
| 247 | +        return byte_file.read()
 | |
| 248 | + | |
| 249 | + | |
| 250 | +def output_file_maker(file_path, input_path):
 | |
| 251 | +    """Creates an :obj:`OutputFile` from a local file.
 | |
| 252 | + | |
| 253 | +    `file_path` **must** point inside or be relative to `input_path`.
 | |
| 254 | + | |
| 255 | +    Args:
 | |
| 256 | +        file_path (str): absolute or relative path to a local file.
 | |
| 257 | +        input_path (str): absolute or relative path to the input root directory.
 | |
| 258 | + | |
| 259 | +    Returns:
 | |
| 260 | +        :obj:`OutputFile`, :obj:`BatchUpdateBlobsRequest`: Tuple of a new gRPC
 | |
| 261 | +        :obj:`OutputFile` object for the file pointed by `file_path` and the
 | |
| 262 | +        corresponding :obj:`BatchUpdateBlobsRequest` for CAS upload.
 | |
| 263 | +    """
 | |
| 264 | +    if not os.path.isabs(file_path):
 | |
| 265 | +        file_path = os.path.abspath(file_path)
 | |
| 266 | +    if not os.path.isabs(input_path):
 | |
| 267 | +        input_path = os.path.abspath(input_path)
 | |
| 268 | + | |
| 269 | +    file_blob = read_file(file_path)
 | |
| 270 | +    file_digest = create_digest(file_blob)
 | |
| 271 | + | |
| 272 | +    output_file = remote_execution_pb2.OutputFile(digest=file_digest)
 | |
| 273 | +    output_file.path = os.path.relpath(file_path, start=input_path)
 | |
| 274 | +    output_file.is_executable = os.access(file_path, os.X_OK)
 | |
| 275 | + | |
| 276 | +    update_request = remote_execution_pb2.BatchUpdateBlobsRequest.Request(digest=file_digest)
 | |
| 277 | +    update_request.data = file_blob
 | |
| 278 | + | |
| 279 | +    return output_file, update_request
 | |
| 280 | + | |
| 281 | + | |
| 282 | +def output_directory_maker(directory_path, working_path):
 | |
| 283 | +    """Creates an :obj:`OutputDirectory` from a local directory.
 | |
| 284 | + | |
| 285 | +    `directory_path` **must** point inside or be relative to `input_path`.
 | |
| 286 | + | |
| 287 | +    Args:
 | |
| 288 | +        directory_path (str): absolute or relative path to a local directory.
 | |
| 289 | +        working_path (str): absolute or relative path to the working directory.
 | |
| 290 | + | |
| 291 | +    Returns:
 | |
| 292 | +        :obj:`OutputDirectory`, :obj:`BatchUpdateBlobsRequest`: Tuple of a new
 | |
| 293 | +        gRPC :obj:`OutputDirectory` for the directory pointed by
 | |
| 294 | +        `directory_path` and the corresponding list of
 | |
| 295 | +        :obj:`BatchUpdateBlobsRequest` for CAS upload.
 | |
| 296 | +    """
 | |
| 297 | +    if not os.path.isabs(directory_path):
 | |
| 298 | +        directory_path = os.path.abspath(directory_path)
 | |
| 299 | +    if not os.path.isabs(working_path):
 | |
| 300 | +        working_path = os.path.abspath(working_path)
 | |
| 301 | + | |
| 302 | +    _, update_requests = tree_maker(directory_path)
 | |
| 303 | + | |
| 304 | +    output_directory = remote_execution_pb2.OutputDirectory()
 | |
| 305 | +    output_directory.tree_digest.CopyFrom(update_requests[-1].digest)
 | |
| 306 | +    output_directory.path = os.path.relpath(directory_path, start=working_path)
 | |
| 307 | + | |
| 308 | +    output_directory_blob = output_directory.SerializeToString()
 | |
| 309 | +    output_directory_digest = create_digest(output_directory_blob)
 | |
| 310 | + | |
| 311 | +    update_request = remote_execution_pb2.BatchUpdateBlobsRequest.Request(digest=output_directory_digest)
 | |
| 312 | +    update_request.data = output_directory_blob
 | |
| 313 | + | |
| 314 | +    update_requests.append(update_request)
 | |
| 315 | + | |
| 316 | +    return output_directory, update_requests
 | |
| 317 | + | |
| 318 | + | |
| 319 | +def tree_maker(directory_path):
 | |
| 320 | +    """Creates a :obj:`Tree` from a local directory.
 | |
| 321 | + | |
| 322 | +    Args:
 | |
| 323 | +        directory_path (str): absolute or relative path to a local directory.
 | |
| 324 | + | |
| 325 | +    Returns:
 | |
| 326 | +        :obj:`Tree`, :obj:`BatchUpdateBlobsRequest`: Tuple of a new
 | |
| 327 | +        gRPC :obj:`Tree` for the directory pointed by `directory_path` and the
 | |
| 328 | +        corresponding list of :obj:`BatchUpdateBlobsRequest` for CAS upload.
 | |
| 329 | + | |
| 330 | +        The :obj:`BatchUpdateBlobsRequest` list may come in any order. However,
 | |
| 331 | +        its last element is guaranteed to be the :obj:`Tree`'s request.
 | |
| 332 | +    """
 | |
| 333 | +    if not os.path.isabs(directory_path):
 | |
| 334 | +        directory_path = os.path.abspath(directory_path)
 | |
| 335 | + | |
| 336 | +    directory, child_directories, update_requests = directory_maker(directory_path)
 | |
| 337 | + | |
| 338 | +    tree = remote_execution_pb2.Tree()
 | |
| 339 | +    tree.children.extend(child_directories)
 | |
| 340 | +    tree.root.CopyFrom(directory)
 | |
| 341 | + | |
| 342 | +    tree_blob = tree.SerializeToString()
 | |
| 343 | +    tree_digest = create_digest(tree_blob)
 | |
| 344 | + | |
| 345 | +    update_request = remote_execution_pb2.BatchUpdateBlobsRequest.Request(digest=tree_digest)
 | |
| 346 | +    update_request.data = tree_blob
 | |
| 347 | + | |
| 348 | +    update_requests.append(update_request)
 | |
| 349 | + | |
| 350 | +    return tree, update_requests | 
| ... | ... | @@ -90,7 +90,7 @@ tests_require = [ | 
| 90 | 90 |      'moto',
 | 
| 91 | 91 |      'pep8',
 | 
| 92 | 92 |      'pytest == 3.6.4',
 | 
| 93 | -    'pytest-cov >= 2.5.0',
 | |
| 93 | +    'pytest-cov >= 2.6.0',
 | |
| 94 | 94 |      'pytest-pep8',
 | 
| 95 | 95 |      'pytest-pylint',
 | 
| 96 | 96 |  ]
 | 
