Module mbed_project.mbed_program

Mbed Program abstraction layer.

Expand source code
#
# Copyright (C) 2020 Arm Mbed. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
"""Mbed Program abstraction layer."""
import logging

from pathlib import Path
from typing import List, Dict
from urllib.parse import urlparse

from mbed_project.exceptions import ProgramNotFound, ExistingProgram, MbedOSNotFound
from mbed_project._internal import git_utils
from mbed_project._internal.project_data import (
    MbedProgramFiles,
    MbedOS,
    PROGRAM_ROOT_FILE_NAME,
    MBED_OS_DIR_NAME,
)
from mbed_project._internal.libraries import LibraryReferences, MbedLibReference

logger = logging.getLogger(__name__)


class MbedProgram:
    """Represents an Mbed program.

    An `MbedProgram` consists of:
        * A git repository
        * A copy of, or reference to, `MbedOS`
        * A set of `MbedProgramFiles`
        * A collection of references to external libraries, defined in .lib files located in the program source tree
    """

    def __init__(self, repo: git_utils.git.Repo, program_files: MbedProgramFiles, mbed_os: MbedOS) -> None:
        """Initialise the program attributes.

        Args:
            repo: The program's git repository.
            program_files: Object holding paths to a set of files that define an Mbed program.
            mbed_os: An instance of `MbedOS` holding paths to locations in the local copy of the Mbed OS source.
        """
        self.repo = repo
        self.files = program_files
        self.mbed_os = mbed_os
        self.lib_references = LibraryReferences(root=self.files.mbed_file.parent, ignore_paths=[self.mbed_os.root])

    @classmethod
    def from_url(cls, url: str, dst_path: Path, check_mbed_os: bool = True) -> "MbedProgram":
        """Fetch an Mbed program from a remote URL.

        Args:
            url: URL of the remote program repository.
            dst_path: Destination path for the cloned program.

        Raises:
            ExistingProgram: `dst_path` already contains an Mbed program.
        """
        if _tree_contains_program(dst_path):
            raise ExistingProgram(
                f"The destination path '{dst_path}' already contains an Mbed program. Please set the destination path "
                "to an empty directory."
            )
        logger.info(f"Cloning Mbed program from URL '{url}'.")
        repo = git_utils.clone(url, dst_path)

        try:
            program_files = MbedProgramFiles.from_existing(dst_path)
        except ValueError as e:
            raise ProgramNotFound(
                f"This repository does not contain a valid Mbed program at the top level. {e} "
                "Cloned programs must contain an mbed-os.lib file containing the URL to the Mbed OS repository. It is "
                "possible you have cloned a repository containing multiple mbed-programs. If this is the case, you "
                "should cd to a directory containing a program before performing any other operations."
            )

        try:
            mbed_os = MbedOS.from_existing(dst_path / MBED_OS_DIR_NAME, check_mbed_os)
        except ValueError as mbed_err:
            raise MbedOSNotFound(f"{mbed_err}")

        return cls(repo, program_files, mbed_os)

    @classmethod
    def from_new(cls, dir_path: Path) -> "MbedProgram":
        """Create an MbedProgram from an empty directory.

        Creates the directory if it doesn't exist.

        Args:
            dir_path: Directory in which to create the program.

        Raises:
            ExistingProgram: An existing program was found in the path.
        """
        if _tree_contains_program(dir_path):
            raise ExistingProgram(
                f"An existing Mbed program was found in the directory tree {dir_path}. It is not possible to nest Mbed "
                "programs. Please ensure there is no .mbed file in the cwd hierarchy."
            )

        logger.info(f"Creating Mbed program at path '{dir_path.resolve()}'")
        dir_path.mkdir(exist_ok=True)
        program_files = MbedProgramFiles.from_new(dir_path)
        logger.info(f"Creating git repository for the Mbed program '{dir_path}'")
        repo = git_utils.init(dir_path)
        mbed_os = MbedOS.from_new(dir_path / MBED_OS_DIR_NAME)
        return cls(repo, program_files, mbed_os)

    @classmethod
    def from_existing(cls, dir_path: Path, check_mbed_os: bool = True) -> "MbedProgram":
        """Create an MbedProgram from an existing program directory.

        Args:
            dir_path: Directory containing an Mbed program.
            check_mbed_os: If True causes an exception to be raised if the Mbed OS source directory does not
                           exist.

        Raises:
            ProgramNotFound: An existing program was not found in the path.
        """
        program_root = _find_program_root(dir_path)
        logger.info(f"Found existing Mbed program at path '{program_root}'")
        try:
            program = MbedProgramFiles.from_existing(program_root)
        except ValueError as program_files_err:
            raise ProgramNotFound(f"{dir_path} doesn't look like a path to a valid program. {program_files_err}")

        repo = git_utils.get_repo(program_root)
        try:
            mbed_os = MbedOS.from_existing(program_root / MBED_OS_DIR_NAME, check_mbed_os)
        except ValueError as mbed_os_err:
            raise MbedOSNotFound(
                f"Mbed OS was not found due to the following error: {mbed_os_err}"
                "\nYou may need to resolve the mbed-os.lib reference. You can do this by performing a `checkout`."
            )

        return cls(repo, program, mbed_os)

    def resolve_libraries(self) -> None:
        """Resolve all external dependencies defined in .lib files."""
        self.lib_references.resolve()

    def checkout_libraries(self, force: bool = False) -> None:
        """Check out all resolved libraries to revisions specified in .lib files."""
        self.lib_references.checkout(force)

    def list_known_library_dependencies(self) -> List[MbedLibReference]:
        """Returns a list of all known library dependencies."""
        return [lib for lib in self.lib_references.iter_all()]

    def has_unresolved_libraries(self) -> bool:
        """Checks if any unresolved library dependencies exist in the program tree."""
        return bool(list(self.lib_references.iter_unresolved()))


def parse_url(name_or_url: str) -> Dict[str, str]:
    """Create a valid github/armmbed url from a program name.

    Args:
        url: The URL, or a program name to turn into an URL.

    Returns:
        Dictionary containing the remote url and the destination path for the clone.
    """
    url_obj = urlparse(name_or_url)
    if url_obj.hostname:
        url = url_obj.geturl()
    else:
        url = f"https://github.com/armmbed/{url_obj.path}"
    # We need to create a valid directory name from the url path section.
    return {"url": url, "dst_path": url_obj.path.rsplit("/", maxsplit=1)[-1].replace("/", "")}


def _tree_contains_program(path: Path) -> bool:
    """Check if the current path or its ancestors contain a .mbed file.

    Args:
        path: The starting path for the search. The search walks up the tree from this path.

    Returns:
        `True` if a .mbed file is located between `path` and filesystem root.
        `False` if no .mbed file was found.
    """
    try:
        _find_program_root(path)
        return True
    except ProgramNotFound:
        return False


def _find_program_root(cwd: Path) -> Path:
    """Walk up the directory tree, looking for a .mbed file.

    Programs contain a .mbed file at the root of the source tree.

    Args:
        cwd: The directory path to search for a program.

    Raises:
        ProgramNotFound: No .mbed file found in the path.

    Returns:
        Path containing the .mbed file.
    """
    potential_root = cwd.resolve()
    while str(potential_root) != str(potential_root.anchor):
        logger.debug(f"Searching for .mbed file at path {potential_root}")
        root_file = potential_root / PROGRAM_ROOT_FILE_NAME
        if root_file.exists() and root_file.is_file():
            logger.debug(f".mbed file found at {potential_root}")
            return potential_root

        potential_root = potential_root.parent

    logger.debug("No .mbed file found.")
    raise ProgramNotFound(
        f"No program found from {cwd.resolve()} to {cwd.resolve().anchor}. Please set the cwd to a program directory "
        "containing a .mbed file. You can also set your cwd to a program subdirectory if there is a .mbed file at the "
        "root of your program's directory tree. If your program does not contain a .mbed file, please create an empty "
        ".mbed file at the root of the program directory tree before performing any other operations."
    )

Functions

def parse_url(name_or_url: str) -> Dict[str, str]

Create a valid github/armmbed url from a program name.

Args

url
The URL, or a program name to turn into an URL.

Returns

Dictionary containing the remote url and the destination path for the clone.

Expand source code
def parse_url(name_or_url: str) -> Dict[str, str]:
    """Create a valid github/armmbed url from a program name.

    Args:
        url: The URL, or a program name to turn into an URL.

    Returns:
        Dictionary containing the remote url and the destination path for the clone.
    """
    url_obj = urlparse(name_or_url)
    if url_obj.hostname:
        url = url_obj.geturl()
    else:
        url = f"https://github.com/armmbed/{url_obj.path}"
    # We need to create a valid directory name from the url path section.
    return {"url": url, "dst_path": url_obj.path.rsplit("/", maxsplit=1)[-1].replace("/", "")}

Classes

class MbedProgram (repo: git.repo.base.Repo, program_files: mbed_project._internal.project_data.MbedProgramFiles, mbed_os: mbed_project._internal.project_data.MbedOS)

Represents an Mbed program.

An MbedProgram consists of: * A git repository * A copy of, or reference to, MbedOS * A set of MbedProgramFiles * A collection of references to external libraries, defined in .lib files located in the program source tree

Initialise the program attributes.

Args

repo
The program's git repository.
program_files
Object holding paths to a set of files that define an Mbed program.
mbed_os
An instance of MbedOS holding paths to locations in the local copy of the Mbed OS source.
Expand source code
class MbedProgram:
    """Represents an Mbed program.

    An `MbedProgram` consists of:
        * A git repository
        * A copy of, or reference to, `MbedOS`
        * A set of `MbedProgramFiles`
        * A collection of references to external libraries, defined in .lib files located in the program source tree
    """

    def __init__(self, repo: git_utils.git.Repo, program_files: MbedProgramFiles, mbed_os: MbedOS) -> None:
        """Initialise the program attributes.

        Args:
            repo: The program's git repository.
            program_files: Object holding paths to a set of files that define an Mbed program.
            mbed_os: An instance of `MbedOS` holding paths to locations in the local copy of the Mbed OS source.
        """
        self.repo = repo
        self.files = program_files
        self.mbed_os = mbed_os
        self.lib_references = LibraryReferences(root=self.files.mbed_file.parent, ignore_paths=[self.mbed_os.root])

    @classmethod
    def from_url(cls, url: str, dst_path: Path, check_mbed_os: bool = True) -> "MbedProgram":
        """Fetch an Mbed program from a remote URL.

        Args:
            url: URL of the remote program repository.
            dst_path: Destination path for the cloned program.

        Raises:
            ExistingProgram: `dst_path` already contains an Mbed program.
        """
        if _tree_contains_program(dst_path):
            raise ExistingProgram(
                f"The destination path '{dst_path}' already contains an Mbed program. Please set the destination path "
                "to an empty directory."
            )
        logger.info(f"Cloning Mbed program from URL '{url}'.")
        repo = git_utils.clone(url, dst_path)

        try:
            program_files = MbedProgramFiles.from_existing(dst_path)
        except ValueError as e:
            raise ProgramNotFound(
                f"This repository does not contain a valid Mbed program at the top level. {e} "
                "Cloned programs must contain an mbed-os.lib file containing the URL to the Mbed OS repository. It is "
                "possible you have cloned a repository containing multiple mbed-programs. If this is the case, you "
                "should cd to a directory containing a program before performing any other operations."
            )

        try:
            mbed_os = MbedOS.from_existing(dst_path / MBED_OS_DIR_NAME, check_mbed_os)
        except ValueError as mbed_err:
            raise MbedOSNotFound(f"{mbed_err}")

        return cls(repo, program_files, mbed_os)

    @classmethod
    def from_new(cls, dir_path: Path) -> "MbedProgram":
        """Create an MbedProgram from an empty directory.

        Creates the directory if it doesn't exist.

        Args:
            dir_path: Directory in which to create the program.

        Raises:
            ExistingProgram: An existing program was found in the path.
        """
        if _tree_contains_program(dir_path):
            raise ExistingProgram(
                f"An existing Mbed program was found in the directory tree {dir_path}. It is not possible to nest Mbed "
                "programs. Please ensure there is no .mbed file in the cwd hierarchy."
            )

        logger.info(f"Creating Mbed program at path '{dir_path.resolve()}'")
        dir_path.mkdir(exist_ok=True)
        program_files = MbedProgramFiles.from_new(dir_path)
        logger.info(f"Creating git repository for the Mbed program '{dir_path}'")
        repo = git_utils.init(dir_path)
        mbed_os = MbedOS.from_new(dir_path / MBED_OS_DIR_NAME)
        return cls(repo, program_files, mbed_os)

    @classmethod
    def from_existing(cls, dir_path: Path, check_mbed_os: bool = True) -> "MbedProgram":
        """Create an MbedProgram from an existing program directory.

        Args:
            dir_path: Directory containing an Mbed program.
            check_mbed_os: If True causes an exception to be raised if the Mbed OS source directory does not
                           exist.

        Raises:
            ProgramNotFound: An existing program was not found in the path.
        """
        program_root = _find_program_root(dir_path)
        logger.info(f"Found existing Mbed program at path '{program_root}'")
        try:
            program = MbedProgramFiles.from_existing(program_root)
        except ValueError as program_files_err:
            raise ProgramNotFound(f"{dir_path} doesn't look like a path to a valid program. {program_files_err}")

        repo = git_utils.get_repo(program_root)
        try:
            mbed_os = MbedOS.from_existing(program_root / MBED_OS_DIR_NAME, check_mbed_os)
        except ValueError as mbed_os_err:
            raise MbedOSNotFound(
                f"Mbed OS was not found due to the following error: {mbed_os_err}"
                "\nYou may need to resolve the mbed-os.lib reference. You can do this by performing a `checkout`."
            )

        return cls(repo, program, mbed_os)

    def resolve_libraries(self) -> None:
        """Resolve all external dependencies defined in .lib files."""
        self.lib_references.resolve()

    def checkout_libraries(self, force: bool = False) -> None:
        """Check out all resolved libraries to revisions specified in .lib files."""
        self.lib_references.checkout(force)

    def list_known_library_dependencies(self) -> List[MbedLibReference]:
        """Returns a list of all known library dependencies."""
        return [lib for lib in self.lib_references.iter_all()]

    def has_unresolved_libraries(self) -> bool:
        """Checks if any unresolved library dependencies exist in the program tree."""
        return bool(list(self.lib_references.iter_unresolved()))

Static methods

def from_existing(dir_path: pathlib.Path, check_mbed_os: bool = True) -> MbedProgram

Create an MbedProgram from an existing program directory.

Args

dir_path
Directory containing an Mbed program.
check_mbed_os
If True causes an exception to be raised if the Mbed OS source directory does not exist.

Raises

ProgramNotFound
An existing program was not found in the path.
Expand source code
@classmethod
def from_existing(cls, dir_path: Path, check_mbed_os: bool = True) -> "MbedProgram":
    """Create an MbedProgram from an existing program directory.

    Args:
        dir_path: Directory containing an Mbed program.
        check_mbed_os: If True causes an exception to be raised if the Mbed OS source directory does not
                       exist.

    Raises:
        ProgramNotFound: An existing program was not found in the path.
    """
    program_root = _find_program_root(dir_path)
    logger.info(f"Found existing Mbed program at path '{program_root}'")
    try:
        program = MbedProgramFiles.from_existing(program_root)
    except ValueError as program_files_err:
        raise ProgramNotFound(f"{dir_path} doesn't look like a path to a valid program. {program_files_err}")

    repo = git_utils.get_repo(program_root)
    try:
        mbed_os = MbedOS.from_existing(program_root / MBED_OS_DIR_NAME, check_mbed_os)
    except ValueError as mbed_os_err:
        raise MbedOSNotFound(
            f"Mbed OS was not found due to the following error: {mbed_os_err}"
            "\nYou may need to resolve the mbed-os.lib reference. You can do this by performing a `checkout`."
        )

    return cls(repo, program, mbed_os)
def from_new(dir_path: pathlib.Path) -> MbedProgram

Create an MbedProgram from an empty directory.

Creates the directory if it doesn't exist.

Args

dir_path
Directory in which to create the program.

Raises

ExistingProgram
An existing program was found in the path.
Expand source code
@classmethod
def from_new(cls, dir_path: Path) -> "MbedProgram":
    """Create an MbedProgram from an empty directory.

    Creates the directory if it doesn't exist.

    Args:
        dir_path: Directory in which to create the program.

    Raises:
        ExistingProgram: An existing program was found in the path.
    """
    if _tree_contains_program(dir_path):
        raise ExistingProgram(
            f"An existing Mbed program was found in the directory tree {dir_path}. It is not possible to nest Mbed "
            "programs. Please ensure there is no .mbed file in the cwd hierarchy."
        )

    logger.info(f"Creating Mbed program at path '{dir_path.resolve()}'")
    dir_path.mkdir(exist_ok=True)
    program_files = MbedProgramFiles.from_new(dir_path)
    logger.info(f"Creating git repository for the Mbed program '{dir_path}'")
    repo = git_utils.init(dir_path)
    mbed_os = MbedOS.from_new(dir_path / MBED_OS_DIR_NAME)
    return cls(repo, program_files, mbed_os)
def from_url(url: str, dst_path: pathlib.Path, check_mbed_os: bool = True) -> MbedProgram

Fetch an Mbed program from a remote URL.

Args

url
URL of the remote program repository.
dst_path
Destination path for the cloned program.

Raises

ExistingProgram
dst_path already contains an Mbed program.
Expand source code
@classmethod
def from_url(cls, url: str, dst_path: Path, check_mbed_os: bool = True) -> "MbedProgram":
    """Fetch an Mbed program from a remote URL.

    Args:
        url: URL of the remote program repository.
        dst_path: Destination path for the cloned program.

    Raises:
        ExistingProgram: `dst_path` already contains an Mbed program.
    """
    if _tree_contains_program(dst_path):
        raise ExistingProgram(
            f"The destination path '{dst_path}' already contains an Mbed program. Please set the destination path "
            "to an empty directory."
        )
    logger.info(f"Cloning Mbed program from URL '{url}'.")
    repo = git_utils.clone(url, dst_path)

    try:
        program_files = MbedProgramFiles.from_existing(dst_path)
    except ValueError as e:
        raise ProgramNotFound(
            f"This repository does not contain a valid Mbed program at the top level. {e} "
            "Cloned programs must contain an mbed-os.lib file containing the URL to the Mbed OS repository. It is "
            "possible you have cloned a repository containing multiple mbed-programs. If this is the case, you "
            "should cd to a directory containing a program before performing any other operations."
        )

    try:
        mbed_os = MbedOS.from_existing(dst_path / MBED_OS_DIR_NAME, check_mbed_os)
    except ValueError as mbed_err:
        raise MbedOSNotFound(f"{mbed_err}")

    return cls(repo, program_files, mbed_os)

Methods

def checkout_libraries(self, force: bool = False) -> NoneType

Check out all resolved libraries to revisions specified in .lib files.

Expand source code
def checkout_libraries(self, force: bool = False) -> None:
    """Check out all resolved libraries to revisions specified in .lib files."""
    self.lib_references.checkout(force)
def has_unresolved_libraries(self) -> bool

Checks if any unresolved library dependencies exist in the program tree.

Expand source code
def has_unresolved_libraries(self) -> bool:
    """Checks if any unresolved library dependencies exist in the program tree."""
    return bool(list(self.lib_references.iter_unresolved()))
def list_known_library_dependencies(self) -> List[mbed_project._internal.libraries.MbedLibReference]

Returns a list of all known library dependencies.

Expand source code
def list_known_library_dependencies(self) -> List[MbedLibReference]:
    """Returns a list of all known library dependencies."""
    return [lib for lib in self.lib_references.iter_all()]
def resolve_libraries(self) -> NoneType

Resolve all external dependencies defined in .lib files.

Expand source code
def resolve_libraries(self) -> None:
    """Resolve all external dependencies defined in .lib files."""
    self.lib_references.resolve()