From nobody Tue Feb 10 21:59:30 2026 Delivered-To: importer@patchew.org Received-SPF: pass (zohomail.com: domain of groups.io designates 66.175.222.108 as permitted sender) client-ip=66.175.222.108; envelope-from=bounce+27952+76917+1787277+3901457@groups.io; helo=mail02.groups.io; Authentication-Results: mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of groups.io designates 66.175.222.108 as permitted sender) smtp.mailfrom=bounce+27952+76917+1787277+3901457@groups.io; dmarc=fail(p=none dis=none) header.from=arm.com ARC-Seal: i=1; a=rsa-sha256; t=1624435373; cv=none; d=zohomail.com; s=zohoarc; b=k4185MWjXGr0AUA9gjbSJNCx2GOjDT5wQ/WaqnkaOwM39LfygaFyKhoNEVDnIcG+Yjv5UIVwuXKyCli4QO6LuyWHX/V5M82Xl410ZN+CjP/FLn5P7fGFCmA7LABoVa4uB6rGPacVvTZ/crrgA2vpAjVF+tW1DQxpymAqmeFE9UY= ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=zohomail.com; s=zohoarc; t=1624435373; h=Date:From:In-Reply-To:List-Subscribe:List-Id:List-Help:List-Unsubscribe:Message-ID:Reply-To:References:Sender:Subject:To; bh=Ndfxlyt2xYCnROBeOB93n/VkvZ1dT0EctR24RMymtm0=; b=XSy+ceA1uH0lZcIRa/MFfuJoPyLR8Z076WqvuRa5uzY5vs0Q0AE+MK1HDNtqKa9gATTearzFp+8YwQoLejg28YHUmixLLNPsQL4uqLzsKAcdzYVsRBbCBP17PJgPTBIE5M+mHJIbhXPdX85xhJuWE23PPMQrgoDTf8xwh5XM1Q4= ARC-Authentication-Results: i=1; mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of groups.io designates 66.175.222.108 as permitted sender) smtp.mailfrom=bounce+27952+76917+1787277+3901457@groups.io; dmarc=fail header.from= (p=none dis=none) Received: from mail02.groups.io (mail02.groups.io [66.175.222.108]) by mx.zohomail.com with SMTPS id 1624435373057575.1034605381302; Wed, 23 Jun 2021 01:02:53 -0700 (PDT) Return-Path: X-Received: by 127.0.0.2 with SMTP id j7A0YY1788612xcfx03oaIkX; Wed, 23 Jun 2021 01:02:52 -0700 X-Received: from foss.arm.com (foss.arm.com [217.140.110.172]) by mx.groups.io with SMTP id smtpd.web08.4553.1624435372141996748 for ; Wed, 23 Jun 2021 01:02:52 -0700 X-Received: from usa-sjc-imap-foss1.foss.arm.com (unknown [10.121.207.14]) by usa-sjc-mx-foss1.foss.arm.com (Postfix) with ESMTP id C1630113E; Wed, 23 Jun 2021 01:02:51 -0700 (PDT) X-Received: from e120189.arm.com (unknown [10.57.78.245]) by usa-sjc-imap-foss1.foss.arm.com (Postfix) with ESMTPA id 07D413F719; Wed, 23 Jun 2021 01:02:49 -0700 (PDT) From: "PierreGondois" To: devel@edk2.groups.io, Sean Brogan , Bret Barkelew , Michael D Kinney , Liming Gao , Sami Mujawar Subject: [edk2-devel] [PATCH edk2-platforms v1 3/6] .pytool/Plugin: Add CI plugins Date: Wed, 23 Jun 2021 09:02:06 +0100 Message-Id: <20210623080209.28380-4-Pierre.Gondois@arm.com> In-Reply-To: <20210623080209.28380-1-Pierre.Gondois@arm.com> References: <20210623080209.28380-1-Pierre.Gondois@arm.com> Precedence: Bulk List-Unsubscribe: List-Subscribe: List-Help: Sender: devel@edk2.groups.io List-Id: Mailing-List: list devel@edk2.groups.io; contact devel+owner@edk2.groups.io Reply-To: devel@edk2.groups.io,pierre.gondois@arm.com X-Gm-Message-State: 5w6rIRsDWwtvZUhL1m4MZnWqx1787277AA= DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=groups.io; q=dns/txt; s=20140610; t=1624435372; bh=IsFKjVKbfi1OXoIRtj2JjcOiydwW1KnLmsctza+hJi8=; h=Date:From:Reply-To:Subject:To; b=XrIHvXk0qmXFRVfoGZgtQZLuhdARl/fhVLgkujmlfcZQWZMYCbQ+/iPkB7Jgq5NvsZZ j8kyVPiEQjtjypzhzzi3hFj4jD1R1Hg5eF3GJ3POwQt1g88HjbMrkXC1/HKqDFOXrdCiG Odcf0b0kC/1ESR0OSfxzMGzipR7PqkqW0No= X-ZohoMail-DKIM: pass (identity @groups.io) Content-Transfer-Encoding: quoted-printable MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" From: Pierre Gondois To enable CI support of the tianocore/edk2-platforms repository, add a .pytool directory containing the following files: - .pytool/CISettings.py - .pytool/Readme.md These files are largely inspired from the same files available in the edk2 repository. The .pytool/Plugin/* files containing the CI tests to run are not copied. edk2-platforms will rely on the edk2basetools python package and on the edk2 python files, as edk2 is imported as a submodule of edk2-platforms. Cc: Sean Brogan Cc: Bret Barkelew Cc: Michael D Kinney Cc: Liming Gao Cc: Sami Mujawar Signed-off-by: Sami Mujawar Signed-off-by: Pierre Gondois --- .pytool/CISettings.py | 185 ++++++++++++++++++++++++++++ .pytool/Readme.md | 271 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 456 insertions(+) create mode 100644 .pytool/CISettings.py create mode 100644 .pytool/Readme.md diff --git a/.pytool/CISettings.py b/.pytool/CISettings.py new file mode 100644 index 000000000000..18604646030f --- /dev/null +++ b/.pytool/CISettings.py @@ -0,0 +1,185 @@ +# @file +# +# Copyright (c) Microsoft Corporation. +# Copyright (c) 2020, Hewlett Packard Enterprise Development LP. All right= s reserved.
+# Copyright (c) 2020 - 2021, ARM Limited. All rights reserved.
+# SPDX-License-Identifier: BSD-2-Clause-Patent +## +import os +import logging +import edk2basetools + +from edk2toolext.environment import shell_environment +from edk2toolext.invocables.edk2_ci_build import CiBuildSettingsManager +from edk2toolext.invocables.edk2_setup import SetupSettingsManager, Requir= edSubmodule +from edk2toolext.invocables.edk2_update import UpdateSettingsManager +from edk2toolext.invocables.edk2_pr_eval import PrEvalSettingsManager +from edk2toollib.utility_functions import GetHostInfo + + +class Settings(CiBuildSettingsManager, UpdateSettingsManager, SetupSetting= sManager, PrEvalSettingsManager): + + def __init__(self): + self.ActualPackages =3D [] + self.ActualTargets =3D [] + self.ActualArchitectures =3D [] + self.ActualToolChainTag =3D "" + self.ActualScopes =3D None + + # ####################################################################= ################### # + # Extra CmdLine configuration = # + # ####################################################################= ################### # + + def AddCommandLineOptions(self, parserObj): + pass + def RetrieveCommandLineOptions(self, args): + pass + + # ####################################################################= ################### # + # Default Support for this Ci Build = # + # ####################################################################= ################### # + + def GetPackagesSupported(self): + ''' return iterable of edk2 packages supported by this build. + These should be edk2 workspace relative paths ''' + return ( + "JunoPkg", + "VExpressPkg" + ) + + def GetArchitecturesSupported(self): + ''' return iterable of edk2 architectures supported by this build = ''' + return ( + "IA32", + "X64", + "ARM", + "AARCH64", + "RISCV64") + + def GetTargetsSupported(self): + ''' return iterable of edk2 target tags supported by this build ''' + return ("DEBUG", "RELEASE", "NO-TARGET", "NOOPT") + + # ####################################################################= ################### # + # Verify and Save requested Ci Build Config = # + # ####################################################################= ################### # + + def SetPackages(self, list_of_requested_packages): + ''' Confirm the requested package list is valid and configure Sett= ingsManager + to build the requested packages. + + Raise UnsupportedException if a requested_package is not supported + ''' + unsupported =3D set(list_of_requested_packages) - \ + set(self.GetPackagesSupported()) + if(len(unsupported) > 0): + logging.critical( + "Unsupported Package Requested: " + " ".join(unsupported)) + raise Exception("Unsupported Package Requested: " + + " ".join(unsupported)) + self.ActualPackages =3D list_of_requested_packages + + def SetArchitectures(self, list_of_requested_architectures): + ''' Confirm the requests architecture list is valid and configure = SettingsManager + to run only the requested architectures. + + Raise Exception if a list_of_requested_architectures is not suppor= ted + ''' + unsupported =3D set(list_of_requested_architectures) - \ + set(self.GetArchitecturesSupported()) + if(len(unsupported) > 0): + logging.critical( + "Unsupported Architecture Requested: " + " ".join(unsuppor= ted)) + raise Exception( + "Unsupported Architecture Requested: " + " ".join(unsuppor= ted)) + self.ActualArchitectures =3D list_of_requested_architectures + + def SetTargets(self, list_of_requested_target): + ''' Confirm the request target list is valid and configure Setting= sManager + to run only the requested targets. + + Raise UnsupportedException if a requested_target is not supported + ''' + unsupported =3D set(list_of_requested_target) - \ + set(self.GetTargetsSupported()) + if(len(unsupported) > 0): + logging.critical( + "Unsupported Targets Requested: " + " ".join(unsupported)) + raise Exception("Unsupported Targets Requested: " + + " ".join(unsupported)) + self.ActualTargets =3D list_of_requested_target + + # ####################################################################= ################### # + # Actual Configuration for Ci Build = # + # ####################################################################= ################### # + + def GetActiveScopes(self): + ''' return tuple containing scopes that should be active for this = process ''' + if self.ActualScopes is None: + scopes =3D ("cibuild", "edk2-build", "host-based-test") + + self.ActualToolChainTag =3D shell_environment.GetBuildVars().G= etValue("TOOL_CHAIN_TAG", "") + + is_linux =3D GetHostInfo().os.upper() =3D=3D "LINUX" + scopes +=3D ('pipbuild-unix',) if is_linux else ('pipbuild-win= ',) + + if is_linux and self.ActualToolChainTag.upper().startswith("GC= C"): + if "AARCH64" in self.ActualArchitectures: + scopes +=3D ("gcc_aarch64_linux",) + if "ARM" in self.ActualArchitectures: + scopes +=3D ("gcc_arm_linux",) + if "RISCV64" in self.ActualArchitectures: + scopes +=3D ("gcc_riscv64_unknown",) + self.ActualScopes =3D scopes + return self.ActualScopes + + def GetRequiredSubmodules(self): + ''' return iterable containing RequiredSubmodule objects. + If no RequiredSubmodules return an empty iterable + ''' + rs =3D [] + rs.append(RequiredSubmodule( + "edk2", True)) + rs.append(RequiredSubmodule( + "Silicon/RISC-V/ProcessorPkg/Library/RiscVOpensbiLib/opensbi",= False)) + return rs + + def GetName(self): + return "Edk2-platforms" + + def GetDependencies(self): + return [ + ] + + def GetPackagesPath(self): + ''' Return a list of workspace relative paths that should be mappe= d as edk2 PackagesPath ''' + edk2_platforms_path =3D os.path.dirname(os.path.dirname(os.path.ab= spath(__file__))) + return [ + edk2_platforms_path, + os.path.join(edk2_platforms_path, "Platform", "ARM"), + os.path.join(edk2_platforms_path, "edk2") + ] + + def GetWorkspaceRoot(self): + ''' get WorkspacePath ''' + return os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + def FilterPackagesToTest(self, changedFilesList: list, potentialPackag= esList: list) -> list: + ''' Filter potential packages to test based on changed files. ''' + build_these_packages =3D [] + possible_packages =3D potentialPackagesList.copy() + for f in changedFilesList: + # split each part of path for comparison later + nodes =3D f.split("/") + + # python file change in .pytool folder causes building all + if f.endswith(".py") and ".pytool" in nodes: + build_these_packages =3D possible_packages + break + + # BaseTools files that might change the build + if "BaseTools" in nodes: + if os.path.splitext(f) not in [".txt", ".md"]: + build_these_packages =3D possible_packages + break + return build_these_packages diff --git a/.pytool/Readme.md b/.pytool/Readme.md new file mode 100644 index 000000000000..5b3d9679d7f2 --- /dev/null +++ b/.pytool/Readme.md @@ -0,0 +1,271 @@ +# Edk2-platforms Continuous Integration + +## Basic Status + +| Package | Windows VS2019 (IA32/X64)| Ubuntu GCC (IA= 32/X64/ARM/AARCH64) | Known Issues | +| :---- | :----- | :---- = | :--- | +| Platfrom/ARM/JunoPkg | | :heavy_check_m= ark: | Spell checking in audit mode. CompilerCheck disabled = (need a PlatformCI). + +For more detailed status look at the test results of the latest CI run on = the +repo readme. + +## Background + +This Continuous integration and testing infrastructure leverages the Tiano= Core EDKII Tools PIP modules: +[library](https://pypi.org/project/edk2-pytool-library/) and +[extensions](https://pypi.org/project/edk2-pytool-extensions/) (with repos +located [here](https://github.com/tianocore/edk2-pytool-library) and +[here](https://github.com/tianocore/edk2-pytool-extensions)). + +The primary execution flows can be found in the +`.azurepipelines/Windows-VS2019.yml` and `.azurepipelines/Ubuntu-GCC5.yml` +files. These YAML files are consumed by the Azure Dev Ops Build Pipeline a= nd +dictate what server resources should be used, how they should be configure= d, and +what processes should be run on them. An overview of this schema can be fo= und +[here](https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema= ?view=3Dazure-devops&tabs=3Dschema). + +Inspection of these files reveals the EDKII Tools commands that make up the +primary processes for the CI build: 'stuart_setup', 'stuart_update', and +'stuart_ci_build'. These commands come from the EDKII Tools PIP modules an= d are +configured as described below. More documentation on the tools can be +found [here](https://github.com/tianocore/edk2-pytool-extensions/blob/mast= er/docs/using.md) +and [here](https://github.com/tianocore/edk2-pytool-extensions/blob/master= /docs/features/feature_invocables.md). + +## Configuration + +Configuration of the CI process consists of (in order of precedence): + +* command-line arguments passed in via the Pipeline YAML +* a per-package configuration file (e.g. `.ci.yaml`) that is + detected by the CI system in EDKII Tools. +* a global configuration Python module (e.g. `CISetting.py`) passed in via= the + command-line + +The global configuration file is described in +[this readme](https://github.com/tianocore/edk2-pytool-extensions/blob/mas= ter/docs/usability/using_settings_manager.md) +from the EDKII Tools documentation. This configuration is written as a Pyt= hon +module so that decisions can be made dynamically based on command line +parameters and codebase state. + +The per-package configuration file can override most settings in the global +configuration file, but is not dynamic. This file can be used to skip or +customize tests that may be incompatible with a specific package. Each te= st generally requires +per package configuration which comes from this file. + +## Running CI locally + +The EDKII Tools environment (and by extension the ci) is designed to suppo= rt +easily and consistently running locally and in a cloud ci environment. To= do +that a few steps should be followed. Details of EDKII Tools can be found = in the +[docs folder here](https://github.com/tianocore/edk2-pytool-extensions/tre= e/master/docs) + +### Prerequisets + +1. A supported toolchain (others might work but this is what is tested and= validated) + * Windows 10: + * VS 2017 or VS 2019 + * Windows SDK (for rc) + * Windows WDK (for capsules) + * Ubuntu 18.04 or Fedora + * GCC5 + * Easy to add more but this is the current state +2. Python 3.7.x or newer on path +3. git on path +4. Recommended to setup and activate a python virtual environment +5. Install the requirements `pip install --upgrade pip-requirements.txt` + +### Running CI + +1. clone your edk2-platforms repo +2. Activate your python virtual environment in cmd window +3. Get code dependencies (done only when submodules change) + * `stuart_setup -c .pytool/CISettings.py TOOL_CHAIN_TAG=3D` +4. Update other dependencies (done more often) + * `stuart_update -c .pytool/CISettings.py TOOL_CHAIN_TAG=3D` +5. Run CI build (--help will give you options) + * `stuart_ci_build -c .pytool/CISettings.py TOOL_CHAIN_TAG=3D` + * -p : To build only certain packages use a CSV list + * -a : To run only certain architectures use a CSV l= ist + * -t : To run only tests related to certain targets us= e a + CSV list + * By default all tests are opted in. Then given a package.ci.yaml file= those + tests can be configured for a package. Finally setting the check to t= he + value `skip` will skip that plugin. Examples: + * `CompilerPlugin=3Dskip` skip the build test + * `GuidCheck=3Dskip` skip the Guid check + * `SpellCheck=3Dskip` skip the spell checker + * etc +6. Detailed reports and logs per package are captured in the `Build` direc= tory + +## Current PyTool Test Capabilities + +All CI tests are instances of EDKII Tools plugins. Documentation on the pl= ugin +system can be found [here](https://github.com/tianocore/edk2-pytool-extens= ions/blob/master/docs/usability/using_plugin_manager.md) +and [here](https://github.com/tianocore/edk2-pytool-extensions/blob/master= /docs/features/feature_plugin_manager.md). +Upon invocation, each plugin will be passed the path to the current package +under test and a dictionary containing its targeted configuration, as asse= mbled +from the command line, per-package configuration, and global configuration. + +Note: CI plugins are considered unique from build plugins and helper plugi= ns, +even though some CI plugins may execute steps of a build. + +In the example, these plugins live alongside the code under test (in the +`.pytool/Plugin` directory), but may be moved to the 'edk2-test' repo if t= hat +location makes more sense for the community. + +### Module Inclusion Test - DscCompleteCheck + +This scans all INF files from a package and confirms they are +listed in the package level DSC file. The test considers it an error if an= y INF +does not appear in the `Components` section of the package-level DSC (indi= cating +that it would not be built if the package were built). This is critical be= cause +much of the CI infrastructure assumes that all modules will be listed in t= he DSC +and compiled. + +This test will ignore INFs in the following cases: + +1. When `MODULE_TYPE` =3D `HOST_APPLICATION` +2. When a Library instance **only** supports the `HOST_APPLICATION` enviro= nment + +### Host Module Inclusion Test - HostUnitTestDscCompleteCheck + +This test scans all INF files from a package for those related to host +based unit tests and confirms they are listed in the unit test DSC file fo= r the package. +The test considers it an error if any INF meeting the requirements does no= t appear +in the `Components` section of the unit test DSC. This is critical because +much of the CI infrastructure assumes that modules will be listed in the = DSC +and compiled. + +This test will only require INFs in the following cases: + +1. When `MODULE_TYPE` =3D `HOST_APPLICATION` +2. When a Library instance explicitly supports the `HOST_APPLICATION` envi= ronment + +### Code Compilation Test - CompilerPlugin + +Once the Module Inclusion Test has verified that all modules would be buil= t if +all package-level DSCs were built, the Code Compilation Test simply runs t= hrough +and builds every package-level DSC on every toolchain and for every archit= ecture +that is supported. Any module that fails to build is considered an error. + +### Host Unit Test Compilation and Run Test - HostUnitTestCompilerPlugin + +A test that compiles the dsc for host based unit test apps. +On Windows this will also enable a build plugin to execute that will run t= he unit tests and verify the results. + +These tools will be invoked on any CI +pass that includes the NOOPT target. In order for these tools to do their = job, +the package and tests must be configured in a particular way... + +#### Including Host-Based Tests in the Package YAML + +For example, looking at the `MdeModulePkg.ci.yaml` config file, there are = two +config options that control HostBased test behavior: + +```json + ## options defined .pytool/Plugin/HostUnitTestCompilerPlugin + "HostUnitTestCompilerPlugin": { + "DscPath": "Test/MdeModulePkgHostTest.dsc" + }, +``` + +This option tell the test builder to run. The test builder needs to know w= hich +modules in this package are host-based tests, so that DSC path is provided. + +#### Configuring the HostBased DSC + +The HostBased DSC for `MdeModulePkg` is located at +`MdeModulePkg/Test/MdeModulePkgHostTest.dsc`. + +To add automated host-based unit test building to a new package, create a +similar DSC. The new DSC should make sure to have the `NOOPT` BUILD_TARGET +and should include the line: + +``` +!include UnitTestFrameworkPkg/UnitTestFrameworkPkgHost.dsc.inc +``` + +All of the modules that are included in the `Components` section of this +DSC should be of type HOST_APPLICATION. + +### GUID Uniqueness Test - GuidCheck + +This test works on the collection of all packages rather than an individual +package. It looks at all FILE_GUIDs and GUIDs declared in DEC files and en= sures +that they are unique for the codebase. This prevents, for example, acciden= tal +duplication of GUIDs when using an existing INF as a template for a new mo= dule. + +### Cross-Package Dependency Test - DependencyCheck + +This test compares the list of all packages used in INFs files for a given +package against a list of "allowed dependencies" in plugin configuration f= or +that package. Any module that depends on a disallowed package will cause a= test +failure. + +### Library Declaration Test - LibraryClassCheck + +This test scans at all library header files found in the `Library` folders= in +all of the package's declared include directories and ensures that all fil= es +have a matching LibraryClass declaration in the DEC file for the package. = Any +missing declarations will cause a failure. + +### Invalid Character Test - CharEncodingCheck + +This test scans all files in a package to make sure that there are no inva= lid +Unicode characters that may cause build errors in some character +sets/localizations. + +### Spell Checking - cspell + +This test runs a spell checker on all files within the package. This is d= one +using the NodeJs cspell tool. For details check `.pytool/Plugin/SpellChec= k`. +For this plugin to run during ci you must install nodejs and cspell and ha= ve +both available to the command line when running your CI. + +Install + +* Install nodejs from https://nodejs.org/en/ +* Install cspell + 1. Open cmd prompt with access to node and npm + 2. Run `npm install -g cspell` + + More cspell info: https://github.com/streetsidesoftware/cspell + +### License Checking - LicenseCheck + +Scans all new added files in a package to make sure code is contributed un= der +BSD-2-Clause-Patent. + +### Ecc tool - EccCheck + +Run the Ecc tool on the package. The Ecc tool is available in the BaseTools +package. It checks that the code complies to the EDKII coding standard. + +## PyTool Scopes + +Scopes are how the PyTool ext_dep, path_env, and plugins are activated. M= eaning +that if an invocable process has a scope active then those ext_dep and pat= h_env +will be active. To allow easy integration of PyTools capabilities there ar= e a +few standard scopes. + +| Scope | Invocable | Desc= ription | +| :---- | :----- | :---= - | +| global | edk2_invocable++ - should be base_abstract_invocable | Runn= ing an invocables | +| global-win | edk2_invocable++ | Runn= ing on Microsoft Windows | +| global-nix | edk2_invocable++ | Runn= ing on Linux based OS | +| edk2-build | | This= indicates that an invocable is building EDK2 based UEFI code | +| cibuild | set in .pytool/CISettings.py | Sugg= ested target for edk2 continuous integration builds. Tools used for CiBuil= ds can use this scope. Example: asl compiler | +| host-based-test | set in .pytool/CISettings.py | Turn= s on the host based tests and plugin | +| host-test-win | set in .pytool/CISettings.py | Enab= les the host based test runner for Windows | + +## Future investments + +* PatchCheck tests as plugins +* MacOS/xcode support +* Clang/LLVM support +* Visual Studio AARCH64 and ARM support +* BaseTools C tools CI/PR and binary release process +* BaseTools Python tools CI/PR process +* Extensible private/closed source platform reporting +* UEFI SCTs +* Other automation --=20 2.17.1 -=3D-=3D-=3D-=3D-=3D-=3D-=3D-=3D-=3D-=3D-=3D- Groups.io Links: You receive all messages sent to this group. View/Reply Online (#76917): https://edk2.groups.io/g/devel/message/76917 Mute This Topic: https://groups.io/mt/83733298/1787277 Group Owner: devel+owner@edk2.groups.io Unsubscribe: https://edk2.groups.io/g/devel/unsub [importer@patchew.org] -=3D-=3D-=3D-=3D-=3D-=3D-=3D-=3D-=3D-=3D-=3D-