Source code for craft_parts.plugins.dotnet_v2_plugin

# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright 2021,2024 Canonical Ltd.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License version 3 as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""The new .NET plugin."""

import logging
import re
from enum import Enum
from typing import Literal, cast

import pydantic
from overrides import override

from craft_parts.utils.formatting_utils import humanize_list

from . import validator
from .base import Plugin
from .properties import PluginProperties

logger = logging.getLogger(__name__)

_DEBIAN_ARCH_TO_DOTNET_RID: dict[str, str] = {
    "amd64": "linux-x64",
    "arm64": "linux-arm64",
}


[docs] class DotnetConfiguration(str, Enum): """The .NET build configuration.""" DEBUG = "Debug" RELEASE = "Release"
[docs] class DotnetVerbosity(str, Enum): """The .NET build verbosity level.""" QUIET = "quiet" QUIET_SHORT = "q" MINIMAL = "minimal" MINIMAL_SHORT = "m" NORMAL = "normal" NORMAL_SHORT = "n" DETAILED = "detailed" DETAILED_SHORT = "d" DIAGNOSTIC = "diagnostic" DIAGNOSTIC_SHORT = "diag"
[docs] class DotnetV2PluginProperties(PluginProperties, frozen=True): """The part properties used by the .NET plugin.""" plugin: Literal["dotnet"] = "dotnet" # Global flags dotnet_configuration: DotnetConfiguration = DotnetConfiguration.RELEASE dotnet_project: str | None = None dotnet_properties: dict[str, str] = {} dotnet_self_contained: bool = False dotnet_verbosity: DotnetVerbosity = DotnetVerbosity.NORMAL dotnet_version: str | None = None # Restore specific flags dotnet_restore_configfile: str | None = None dotnet_restore_properties: dict[str, str] = {} dotnet_restore_sources: list[str] = [] # Build specific flags dotnet_build_framework: str | None = None dotnet_build_properties: dict[str, str] = {} # Publish specific flags dotnet_publish_properties: dict[str, str] = {} # part properties required by the plugin source: str # pyright: ignore[reportGeneralTypeIssues]
[docs] @pydantic.field_validator("dotnet_version") @classmethod def validate_dotnet_version(cls, value: str | None) -> str | None: """Validate the dotnet-version property. :param value: The value to validate. :return: The validated value. """ if value is None or value == "none": return value # Match either single digit (e.g. "8") or major.minor format (e.g. "8.0") pattern = r"^(\d+)(?:\.(\d+))?$" match = re.match(pattern, value) oldest_supported_dotnet_version = 6 if match: major = int(match.group(1)) if major >= oldest_supported_dotnet_version: return value raise ValueError(f"Invalid dotnet-version {value!r}")
[docs] class DotnetV2PluginEnvironmentValidator(validator.PluginEnvironmentValidator): """Check the execution environment for the .NET plugin. :param part_name: The part whose build environment is being validated. :param env: A string containing the build step environment setup. """
[docs] @override def validate_environment( self, *, part_dependencies: list[str] | None = None ) -> None: """Ensure the environment contains dependencies needed by the plugin. :param part_dependencies: A list of the parts this part depends on. """ # Validating only if .NET SDK is being provided by user. Otherwise, the plugin # will make sure there is an SDK available according to `dotnet-version`. options = cast(DotnetV2PluginProperties, self._options) if options.dotnet_version is None: self.validate_dependency( dependency="dotnet", plugin_name="dotnet", part_dependencies=part_dependencies, )
[docs] class DotnetV2Plugin(Plugin): """A plugin for .NET projects. The .NET plugin uses the common plugin keywords as well as those for "sources". Additionally, the following plugin-specific keywords can be used: Global Flags: - ``dotnet-configuration`` (string) The .NET build configuration to use. (Default: "Release"). - ``dotnet-project`` (string) The .NET proj or solution file to build. (Default: omitted). - ``dotnet-properties`` (dictionary of strings to strings) The list of MSBuild properties to be used by the restore, build, and publish commands. (Default: empty). - ``dotnet-self-contained`` (bool) Build and publish the project as a self-contained application. (Default: False). - ``dotnet-verbosity`` (string) The verbosity level of the build output. Possible values are q[uiet], m[inimal], n[ormal], d[etailed], and diag[nostic]. (Default: "normal"). - ``dotnet-version`` (string) The version of the .NET SDK to download and use. This parameter is optional, so if no value is specified, the plugin will assume a .NET SDK is being provided and will not attempt to download one. Restore-specific Flags: - ``dotnet-restore-sources`` (string) The URI of the NuGet package sources to use during the restore operation. Multiple values can be specified. This setting overrides all of the sources specified in the nuget.config files. (Default: omitted). - ``dotnet-restore-properties`` (dictionary of strings to strings) The list of MSBuild properties to be used by the restore command. (Default: empty). - ``dotnet-restore-configfile`` (string) The NuGet configuration file (nuget.config) to use. (Default: omitted). Build-specific Flags: - ``dotnet-build-framework`` (string) The target framework to build for. (Default: omitted). - ``dotnet-build-properties`` (dictionary of strings to strings) The list of MSBuild properties to be used by the build command. (Default: empty). Publish-specific Flags: - ``dotnet-publish-properties`` (dictionary of strings to strings) The list of MSBuild properties to be used by the publish command. (Default: empty). """ properties_class = DotnetV2PluginProperties validator_class = DotnetV2PluginEnvironmentValidator
[docs] @override def get_build_snaps(self) -> set[str]: """Return a set of required snaps to install in the build environment.""" options = cast(DotnetV2PluginProperties, self._options) # .NET binary provided by the user if ( "dotnet-deps" in self._part_info.part_dependencies or options.dotnet_version is None ): return set() snap_name = self._generate_snap_name(options) if not snap_name: return set() logger.info(f"Using .NET SDK content snap: {snap_name}") build_snaps = set() build_snaps.add(snap_name) return build_snaps
[docs] @override def get_build_packages(self) -> set[str]: """Return a set of required packages to install in the build environment.""" return set()
[docs] @override def get_build_environment(self) -> dict[str, str]: """Return a dictionary with the environment to use in the build step.""" options = cast(DotnetV2PluginProperties, self._options) environment = { "DOTNET_NOLOGO": "1", } # .NET binary provided by the user if ( "dotnet-deps" in self._part_info.part_dependencies or options.dotnet_version is None ): return environment build_on = self._part_info.project_info.arch_build_on snap_name = self._generate_snap_name(options) _, lib_path = self._get_dotnet_platform_info(build_on, snap_name) if lib_path: environment["LD_LIBRARY_PATH"] = lib_path snap_location = f"/snap/{snap_name}/current" dotnet_path = f"{snap_location}/usr/lib/dotnet" environment["PATH"] = f"{dotnet_path}:${{PATH}}" logger.info("Using environment:") for key, value in environment.items(): logger.info(f" {key}={value}") return environment
[docs] @override def get_build_commands(self) -> list[str]: """Return a list of commands to run during the build step.""" options = cast(DotnetV2PluginProperties, self._options) build_for = self._part_info.project_info.arch_build_for dotnet_rid, _ = self._get_dotnet_platform_info(build_for) # Restore step restore_cmd = self._get_restore_command(dotnet_rid, options) # Build step build_cmd = self._get_build_command(dotnet_rid, options) # Publish step publish_cmd = self._get_publish_command(dotnet_rid, options) return [restore_cmd, build_cmd, publish_cmd]
def _generate_snap_name(self, options: DotnetV2PluginProperties) -> str | None: version = options.dotnet_version if version is None: return None version_split = version.split(".") if len(version_split) == 1: snap_version = f"{version}0" else: major, minor = version.split(".") snap_version = major + minor return f"dotnet-sdk-{snap_version}" def _get_restore_command( self, dotnet_rid: str, options: DotnetV2PluginProperties ) -> str: restore_cmd = ["dotnet", "restore"] if options.dotnet_restore_sources: logger.info(f"Using restore sources: {options.dotnet_restore_sources}") restore_cmd.extend( [f"--source {source}" for source in options.dotnet_restore_sources] ) if options.dotnet_restore_configfile: restore_cmd.append(f"--configfile {options.dotnet_restore_configfile}") restore_cmd.append(f"--verbosity {options.dotnet_verbosity.value}") restore_cmd.append(f"--runtime {dotnet_rid}") for prop_name, prop_value in options.dotnet_properties.items(): restore_cmd.append(f"-p:{prop_name}={prop_value}") for prop_name, prop_value in options.dotnet_restore_properties.items(): restore_cmd.append(f"-p:{prop_name}={prop_value}") if options.dotnet_project: restore_cmd.append(f"{options.dotnet_project}") return " ".join(restore_cmd) def _get_build_command( self, dotnet_rid: str, options: DotnetV2PluginProperties ) -> str: build_cmd = [ "dotnet", "build", f"--configuration {options.dotnet_configuration.value}", "--no-restore", ] if options.dotnet_build_framework: build_cmd.append(f"--framework {options.dotnet_build_framework}") build_cmd.append(f"--verbosity {options.dotnet_verbosity.value}") # Self contained build build_cmd.append(f"--runtime {dotnet_rid}") build_cmd.append(f"--self-contained {options.dotnet_self_contained}") for prop_name, prop_value in options.dotnet_properties.items(): build_cmd.append(f"-p:{prop_name}={prop_value}") for prop_name, prop_value in options.dotnet_build_properties.items(): build_cmd.append(f"-p:{prop_name}={prop_value}") if options.dotnet_project: build_cmd.append(f"{options.dotnet_project}") return " ".join(build_cmd) def _get_publish_command( self, dotnet_rid: str, options: DotnetV2PluginProperties ) -> str: publish_cmd = [ "dotnet", "publish", f"--configuration {options.dotnet_configuration.value}", f"--output {self._part_info.part_install_dir}", f"--verbosity {options.dotnet_verbosity.value}", "--no-restore", "--no-build", ] # Self contained build publish_cmd.append(f" --runtime {dotnet_rid}") publish_cmd.append(f" --self-contained {options.dotnet_self_contained}") for prop_name, prop_value in options.dotnet_properties.items(): publish_cmd.append(f" -p:{prop_name}={prop_value}") for prop_name, prop_value in options.dotnet_publish_properties.items(): publish_cmd.append(f" -p:{prop_name}={prop_value}") if options.dotnet_project: publish_cmd.append(f" {options.dotnet_project}") return " ".join(publish_cmd) def _get_dotnet_platform_info( self, arch: str, snap_name: str | None = None ) -> tuple[str, str | None]: """Get the .NET RID and library path for the given architecture. :param arch: The architecture to get the info for. :param snap_name: The name of the snap containing the .NET SDK. :return: A tuple containing the .NET RID and the LD_LIBRARY_PATH value. :raises ValueError: If the architecture is not supported. """ if arch in _DEBIAN_ARCH_TO_DOTNET_RID: rid = _DEBIAN_ARCH_TO_DOTNET_RID[arch] lib_path = None if snap_name: lib_path = ( f"/snap/{snap_name}/current/lib/{self._part_info.project_info.arch_triplet_build_on}:" f"/snap/{snap_name}/current/usr/lib/{self._part_info.project_info.arch_triplet_build_on}" ) return rid, lib_path raise ValueError( f"Unsupported architecture {arch!r}. Supported architectures are {humanize_list(_DEBIAN_ARCH_TO_DOTNET_RID.keys(), 'and')}." )