diff options
| -rwxr-xr-x | ci/build_test_suites | 22 | ||||
| -rw-r--r-- | ci/build_test_suites.py | 282 | ||||
| -rw-r--r-- | ci/test_mapping_module_retriever.py | 125 | ||||
| -rw-r--r-- | core/android_soong_config_vars.mk | 3 | ||||
| -rw-r--r-- | tools/aconfig/src/storage/flag_table.rs | 6 | ||||
| -rw-r--r-- | tools/aconfig/src/storage/flag_value.rs | 181 | ||||
| -rw-r--r-- | tools/aconfig/src/storage/mod.rs | 20 | ||||
| -rw-r--r-- | tools/aconfig/src/storage/package_table.rs | 4 |
8 files changed, 633 insertions, 10 deletions
diff --git a/ci/build_test_suites b/ci/build_test_suites new file mode 100755 index 0000000000..861065a73a --- /dev/null +++ b/ci/build_test_suites @@ -0,0 +1,22 @@ +# Copyright 2024, The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys + +import build_test_suites + +if __name__ == '__main__': + sys.dont_write_bytecode = True + + build_test_suites.main(sys.argv) diff --git a/ci/build_test_suites.py b/ci/build_test_suites.py new file mode 100644 index 0000000000..e88b420d3f --- /dev/null +++ b/ci/build_test_suites.py @@ -0,0 +1,282 @@ +# Copyright 2024, The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Script to build only the necessary modules for general-tests along + +with whatever other targets are passed in. +""" + +import argparse +from collections.abc import Sequence +import json +import os +import pathlib +import re +import subprocess +import sys +from typing import Any, Dict, Set, Text + +import test_mapping_module_retriever + + +# List of modules that are always required to be in general-tests.zip +REQUIRED_MODULES = frozenset( + ['cts-tradefed', 'vts-tradefed', 'compatibility-host-util', 'soong_zip'] +) + + +def build_test_suites(argv): + args = parse_args(argv) + + if not args.change_info: + build_everything(args) + return + + # Call the class to map changed files to modules to build. + # TODO(lucafarsi): Move this into a replaceable class. + build_affected_modules(args) + + +def parse_args(argv): + argparser = argparse.ArgumentParser() + argparser.add_argument( + 'extra_targets', nargs='*', help='Extra test suites to build.' + ) + argparser.add_argument('--target_product') + argparser.add_argument('--target_release') + argparser.add_argument( + '--with_dexpreopt_boot_img_and_system_server_only', action='store_true' + ) + argparser.add_argument('--dist_dir') + argparser.add_argument('--change_info', nargs='?') + argparser.add_argument('--extra_required_modules', nargs='*') + + return argparser.parse_args() + + +def build_everything(args: argparse.Namespace): + build_command = base_build_command(args) + build_command.append('general-tests') + + run_command(build_command) + + +def build_affected_modules(args: argparse.Namespace): + modules_to_build = find_modules_to_build( + pathlib.Path(args.change_info), args.extra_required_modules + ) + + # Call the build command with everything. + build_command = base_build_command(args) + build_command.extend(modules_to_build) + + run_command(build_command) + + zip_build_outputs(modules_to_build, args.dist_dir) + + +def base_build_command(args: argparse.Namespace) -> list: + build_command = [] + build_command.append('time') + build_command.append('./build/soong/soong_ui.bash') + build_command.append('--make-mode') + build_command.append('dist') + build_command.append('DIST_DIR=' + args.dist_dir) + build_command.append('TARGET_PRODUCT=' + args.target_product) + build_command.append('TARGET_RELEASE=' + args.target_release) + build_command.extend(args.extra_targets) + + return build_command + + +def run_command(args: list[str]) -> str: + result = subprocess.run( + args=args, + text=True, + capture_output=True, + check=False, + ) + # If the process failed, print its stdout and propagate the exception. + if not result.returncode == 0: + print('Build command failed! output:') + print('stdout: ' + result.stdout) + print('stderr: ' + result.stderr) + + result.check_returncode() + return result.stdout + + +def find_modules_to_build( + change_info: pathlib.Path, extra_required_modules: list[Text] +) -> Set[Text]: + changed_files = find_changed_files(change_info) + + test_mappings = test_mapping_module_retriever.GetTestMappings( + changed_files, set() + ) + + # Soong_zip is required to generate the output zip so always build it. + modules_to_build = set(REQUIRED_MODULES) + if extra_required_modules: + modules_to_build.update(extra_required_modules) + + modules_to_build.update(find_affected_modules(test_mappings, changed_files)) + + return modules_to_build + + +def find_changed_files(change_info: pathlib.Path) -> Set[Text]: + with open(change_info) as change_info_file: + change_info_contents = json.load(change_info_file) + + changed_files = set() + + for change in change_info_contents['changes']: + project_path = change.get('projectPath') + '/' + + for revision in change.get('revisions'): + for file_info in revision.get('fileInfos'): + changed_files.add(project_path + file_info.get('path')) + + return changed_files + + +def find_affected_modules( + test_mappings: Dict[str, Any], changed_files: Set[Text] +) -> Set[Text]: + modules = set() + + # The test_mappings object returned by GetTestMappings is organized as + # follows: + # { + # 'test_mapping_file_path': { + # 'group_name' : [ + # 'name': 'module_name', + # ], + # } + # } + for test_mapping in test_mappings.values(): + for group in test_mapping.values(): + for entry in group: + module_name = entry.get('name', None) + + if not module_name: + continue + + file_patterns = entry.get('file_patterns') + if not file_patterns: + modules.add(module_name) + continue + + if matches_file_patterns(file_patterns, changed_files): + modules.add(module_name) + continue + + return modules + + +# TODO(lucafarsi): Share this logic with the original logic in +# test_mapping_test_retriever.py +def matches_file_patterns( + file_patterns: list[Text], changed_files: Set[Text] +) -> bool: + for changed_file in changed_files: + for pattern in file_patterns: + if re.search(pattern, changed_file): + return True + + return False + + +def zip_build_outputs(modules_to_build: Set[Text], dist_dir: Text): + src_top = os.environ.get('TOP', os.getcwd()) + + # Call dumpvars to get the necessary things. + # TODO(lucafarsi): Don't call soong_ui 4 times for this, --dumpvars-mode can + # do it but it requires parsing. + host_out_testcases = get_soong_var('HOST_OUT_TESTCASES') + target_out_testcases = get_soong_var('TARGET_OUT_TESTCASES') + product_out = get_soong_var('PRODUCT_OUT') + soong_host_out = get_soong_var('SOONG_HOST_OUT') + host_out = get_soong_var('HOST_OUT') + + # Call the class to package the outputs. + # TODO(lucafarsi): Move this code into a replaceable class. + host_paths = [] + target_paths = [] + for module in modules_to_build: + host_path = os.path.join(host_out_testcases, module) + if os.path.exists(host_path): + host_paths.append(host_path) + + target_path = os.path.join(target_out_testcases, module) + if os.path.exists(target_path): + target_paths.append(target_path) + + zip_command = ['time', os.path.join(host_out, 'bin', 'soong_zip')] + + # Add host testcases. + zip_command.append('-C') + zip_command.append(os.path.join(src_top, soong_host_out)) + zip_command.append('-P') + zip_command.append('host/') + for path in host_paths: + zip_command.append('-D') + zip_command.append(path) + + # Add target testcases. + zip_command.append('-C') + zip_command.append(os.path.join(src_top, product_out)) + zip_command.append('-P') + zip_command.append('target') + for path in target_paths: + zip_command.append('-D') + zip_command.append(path) + + # TODO(lucafarsi): Push this logic into a general-tests-minimal build command + # Add necessary tools. These are also hardcoded in general-tests.mk. + framework_path = os.path.join(soong_host_out, 'framework') + + zip_command.append('-C') + zip_command.append(framework_path) + zip_command.append('-P') + zip_command.append('host/tools') + zip_command.append('-f') + zip_command.append(os.path.join(framework_path, 'cts-tradefed.jar')) + zip_command.append('-f') + zip_command.append( + os.path.join(framework_path, 'compatibility-host-util.jar') + ) + zip_command.append('-f') + zip_command.append(os.path.join(framework_path, 'vts-tradefed.jar')) + + # Zip to the DIST dir. + zip_command.append('-o') + zip_command.append(os.path.join(dist_dir, 'general-tests.zip')) + + run_command(zip_command) + + +def get_soong_var(var: str) -> str: + value = run_command( + ['./build/soong/soong_ui.bash', '--dumpvar-mode', '--abs', var] + ).strip() + if not value: + raise RuntimeError('Necessary soong variable ' + var + ' not found.') + + return value + + +def main(argv): + build_test_suites(sys.argv) diff --git a/ci/test_mapping_module_retriever.py b/ci/test_mapping_module_retriever.py new file mode 100644 index 0000000000..d2c13c0e7d --- /dev/null +++ b/ci/test_mapping_module_retriever.py @@ -0,0 +1,125 @@ +# Copyright 2024, The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Simple parsing code to scan test_mapping files and determine which +modules are needed to build for the given list of changed files. +TODO(lucafarsi): Deduplicate from artifact_helper.py +""" + +from typing import Any, Dict, Set, Text +import json +import os +import re + +# Regex to extra test name from the path of test config file. +TEST_NAME_REGEX = r'(?:^|.*/)([^/]+)\.config' + +# Key name for TEST_MAPPING imports +KEY_IMPORTS = 'imports' +KEY_IMPORT_PATH = 'path' + +# Name of TEST_MAPPING file. +TEST_MAPPING = 'TEST_MAPPING' + +# Pattern used to identify double-quoted strings and '//'-format comments in +# TEST_MAPPING file, but only double-quoted strings are included within the +# matching group. +_COMMENTS_RE = re.compile(r'(\"(?:[^\"\\]|\\.)*\"|(?=//))(?://.*)?') + + +def FilterComments(test_mapping_file: Text) -> Text: + """Remove comments in TEST_MAPPING file to valid format. + + Only '//' is regarded as comments. + + Args: + test_mapping_file: Path to a TEST_MAPPING file. + + Returns: + Valid json string without comments. + """ + return re.sub(_COMMENTS_RE, r'\1', test_mapping_file) + +def GetTestMappings(paths: Set[Text], + checked_paths: Set[Text]) -> Dict[Text, Dict[Text, Any]]: + """Get the affected TEST_MAPPING files. + + TEST_MAPPING files in source code are packaged into a build artifact + `test_mappings.zip`. Inside the zip file, the path of each TEST_MAPPING file + is preserved. From all TEST_MAPPING files in the source code, this method + locates the affected TEST_MAPPING files based on the given paths list. + + A TEST_MAPPING file may also contain `imports` that import TEST_MAPPING files + from a different location, e.g., + "imports": [ + { + "path": "../folder2" + } + ] + In that example, TEST_MAPPING files inside ../folder2 (relative to the + TEST_MAPPING file containing that imports section) and its parent directories + will also be included. + + Args: + paths: A set of paths with related TEST_MAPPING files for given changes. + checked_paths: A set of paths that have been checked for TEST_MAPPING file + already. The set is updated after processing each TEST_MAPPING file. It's + used to prevent infinite loop when the method is called recursively. + + Returns: + A dictionary of Test Mapping containing the content of the affected + TEST_MAPPING files, indexed by the path containing the TEST_MAPPING file. + """ + test_mappings = {} + + # Search for TEST_MAPPING files in each modified path and its parent + # directories. + all_paths = set() + for path in paths: + dir_names = path.split(os.path.sep) + all_paths |= set( + [os.path.sep.join(dir_names[:i + 1]) for i in range(len(dir_names))]) + # Add root directory to the paths to search for TEST_MAPPING file. + all_paths.add('') + + all_paths.difference_update(checked_paths) + checked_paths |= all_paths + # Try to load TEST_MAPPING file in each possible path. + for path in all_paths: + try: + test_mapping_file = os.path.join(os.path.join(os.getcwd(), path), 'TEST_MAPPING') + # Read content of TEST_MAPPING file. + content = FilterComments(open(test_mapping_file, "r").read()) + test_mapping = json.loads(content) + test_mappings[path] = test_mapping + + import_paths = set() + for import_detail in test_mapping.get(KEY_IMPORTS, []): + import_path = import_detail[KEY_IMPORT_PATH] + # Try the import path as absolute path. + import_paths.add(import_path) + # Try the import path as relative path based on the test mapping file + # containing the import. + norm_import_path = os.path.normpath(os.path.join(path, import_path)) + import_paths.add(norm_import_path) + import_paths.difference_update(checked_paths) + if import_paths: + import_test_mappings = GetTestMappings(import_paths, checked_paths) + test_mappings.update(import_test_mappings) + except (KeyError, FileNotFoundError, NotADirectoryError): + # TEST_MAPPING file doesn't exist in path + pass + + return test_mappings diff --git a/core/android_soong_config_vars.mk b/core/android_soong_config_vars.mk index 6af6f08717..6fd59d9f49 100644 --- a/core/android_soong_config_vars.mk +++ b/core/android_soong_config_vars.mk @@ -189,6 +189,9 @@ endif $(call add_soong_config_var,ANDROID,SYSTEM_OPTIMIZE_JAVA) $(call add_soong_config_var,ANDROID,FULL_SYSTEM_OPTIMIZE_JAVA) +# TODO(b/319697968): Remove this build flag support when metalava fully supports flagged api +$(call soong_config_set,ANDROID,release_hidden_api_exportable_stubs,$(RELEASE_HIDDEN_API_EXPORTABLE_STUBS)) + # Check for SupplementalApi module. ifeq ($(wildcard packages/modules/SupplementalApi),) $(call add_soong_config_var_value,ANDROID,include_nonpublic_framework_api,false) diff --git a/tools/aconfig/src/storage/flag_table.rs b/tools/aconfig/src/storage/flag_table.rs index 595217e977..3545700b6a 100644 --- a/tools/aconfig/src/storage/flag_table.rs +++ b/tools/aconfig/src/storage/flag_table.rs @@ -295,8 +295,6 @@ mod tests { }; assert_eq!(header, &expected_header); - println!("{:?}", &flag_table.as_ref().unwrap().nodes); - let buckets: &Vec<Option<u32>> = &flag_table.as_ref().unwrap().buckets; let expected_bucket: Vec<Option<u32>> = vec![ Some(98), @@ -338,9 +336,7 @@ mod tests { #[test] // this test point locks down the table serialization fn test_serialization() { - let flag_table = create_test_flag_table(); - assert!(flag_table.is_ok()); - let flag_table = flag_table.unwrap(); + let flag_table = create_test_flag_table().unwrap(); let header: &FlagTableHeader = &flag_table.header; let reinterpreted_header = FlagTableHeader::from_bytes(&header.as_bytes()); diff --git a/tools/aconfig/src/storage/flag_value.rs b/tools/aconfig/src/storage/flag_value.rs new file mode 100644 index 0000000000..45f5ec0c08 --- /dev/null +++ b/tools/aconfig/src/storage/flag_value.rs @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use crate::commands::assign_flag_ids; +use crate::protos::ProtoFlagState; +use crate::storage::{self, FlagPackage}; +use anyhow::{anyhow, Result}; + +#[derive(PartialEq, Debug)] +pub struct FlagValueHeader { + pub version: u32, + pub container: String, + pub file_size: u32, + pub num_flags: u32, + pub boolean_value_offset: u32, +} + +impl FlagValueHeader { + fn new(container: &str, num_flags: u32) -> Self { + Self { + version: storage::FILE_VERSION, + container: String::from(container), + file_size: 0, + num_flags, + boolean_value_offset: 0, + } + } + + fn as_bytes(&self) -> Vec<u8> { + let mut result = Vec::new(); + result.extend_from_slice(&self.version.to_le_bytes()); + let container_bytes = self.container.as_bytes(); + result.extend_from_slice(&(container_bytes.len() as u32).to_le_bytes()); + result.extend_from_slice(container_bytes); + result.extend_from_slice(&self.file_size.to_le_bytes()); + result.extend_from_slice(&self.num_flags.to_le_bytes()); + result.extend_from_slice(&self.boolean_value_offset.to_le_bytes()); + result + } +} + +#[derive(PartialEq, Debug)] +pub struct FlagValueList { + pub header: FlagValueHeader, + pub booleans: Vec<bool>, +} + +impl FlagValueList { + pub fn new(container: &str, packages: &[FlagPackage]) -> Result<Self> { + // create list + let num_flags = packages.iter().map(|pkg| pkg.boolean_flags.len() as u32).sum(); + + let mut list = Self { + header: FlagValueHeader::new(container, num_flags), + booleans: vec![false; num_flags as usize], + }; + + for pkg in packages.iter() { + let start_offset = pkg.boolean_offset as usize; + let flag_ids = assign_flag_ids(pkg.package_name, pkg.boolean_flags.iter().copied())?; + for pf in pkg.boolean_flags.iter() { + let fid = flag_ids + .get(pf.name()) + .ok_or(anyhow!(format!("missing flag id for {}", pf.name())))?; + + list.booleans[start_offset + (*fid as usize)] = + pf.state() == ProtoFlagState::ENABLED; + } + } + + // initialize all header fields + list.header.boolean_value_offset = list.header.as_bytes().len() as u32; + list.header.file_size = list.header.boolean_value_offset + num_flags; + + Ok(list) + } + + pub fn as_bytes(&self) -> Vec<u8> { + [ + self.header.as_bytes(), + self.booleans + .iter() + .map(|&v| u8::from(v).to_le_bytes()) + .collect::<Vec<_>>() + .concat(), + ] + .concat() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::storage::{ + group_flags_by_package, tests::parse_all_test_flags, tests::read_str_from_bytes, + tests::read_u32_from_bytes, tests::read_u8_from_bytes, + }; + + impl FlagValueHeader { + // test only method to deserialize back into the header struct + fn from_bytes(bytes: &[u8]) -> Result<Self> { + let mut head = 0; + Ok(Self { + version: read_u32_from_bytes(bytes, &mut head)?, + container: read_str_from_bytes(bytes, &mut head)?, + file_size: read_u32_from_bytes(bytes, &mut head)?, + num_flags: read_u32_from_bytes(bytes, &mut head)?, + boolean_value_offset: read_u32_from_bytes(bytes, &mut head)?, + }) + } + } + + impl FlagValueList { + // test only method to deserialize back into the flag value struct + fn from_bytes(bytes: &[u8]) -> Result<Self> { + let header = FlagValueHeader::from_bytes(bytes)?; + let num_flags = header.num_flags; + let mut head = header.as_bytes().len(); + let booleans = (0..num_flags) + .map(|_| read_u8_from_bytes(bytes, &mut head).unwrap() == 1) + .collect(); + let list = Self { header, booleans }; + Ok(list) + } + } + + pub fn create_test_flag_value_list() -> Result<FlagValueList> { + let caches = parse_all_test_flags(); + let packages = group_flags_by_package(caches.iter()); + FlagValueList::new("system", &packages) + } + + #[test] + // this test point locks down the flag value creation and each field + fn test_list_contents() { + let flag_value_list = create_test_flag_value_list(); + assert!(flag_value_list.is_ok()); + + let header: &FlagValueHeader = &flag_value_list.as_ref().unwrap().header; + let expected_header = FlagValueHeader { + version: storage::FILE_VERSION, + container: String::from("system"), + file_size: 34, + num_flags: 8, + boolean_value_offset: 26, + }; + assert_eq!(header, &expected_header); + + let booleans: &Vec<bool> = &flag_value_list.as_ref().unwrap().booleans; + let expected_booleans: Vec<bool> = vec![false; header.num_flags as usize]; + assert_eq!(booleans, &expected_booleans); + } + + #[test] + // this test point locks down the value list serialization + fn test_serialization() { + let flag_value_list = create_test_flag_value_list().unwrap(); + + let header: &FlagValueHeader = &flag_value_list.header; + let reinterpreted_header = FlagValueHeader::from_bytes(&header.as_bytes()); + assert!(reinterpreted_header.is_ok()); + assert_eq!(header, &reinterpreted_header.unwrap()); + + let reinterpreted_value_list = FlagValueList::from_bytes(&flag_value_list.as_bytes()); + assert!(reinterpreted_value_list.is_ok()); + assert_eq!(&flag_value_list, &reinterpreted_value_list.unwrap()); + } +} diff --git a/tools/aconfig/src/storage/mod.rs b/tools/aconfig/src/storage/mod.rs index a28fccd917..36ea3094d3 100644 --- a/tools/aconfig/src/storage/mod.rs +++ b/tools/aconfig/src/storage/mod.rs @@ -15,6 +15,7 @@ */ pub mod flag_table; +pub mod flag_value; pub mod package_table; use anyhow::{anyhow, Result}; @@ -24,7 +25,9 @@ use std::path::PathBuf; use crate::commands::OutputFile; use crate::protos::{ProtoParsedFlag, ProtoParsedFlags}; -use crate::storage::{flag_table::FlagTable, package_table::PackageTable}; +use crate::storage::{ + flag_table::FlagTable, flag_value::FlagValueList, package_table::PackageTable, +}; pub const FILE_VERSION: u32 = 1; @@ -128,7 +131,13 @@ where let flag_table_file = OutputFile { contents: flag_table.as_bytes(), path: flag_table_file_path }; - Ok(vec![package_table_file, flag_table_file]) + // create and serialize flag value + let flag_value = FlagValueList::new(container, &packages)?; + let flag_value_file_path = PathBuf::from("flag.val"); + let flag_value_file = + OutputFile { contents: flag_value.as_bytes(), path: flag_value_file_path }; + + Ok(vec![package_table_file, flag_table_file, flag_value_file]) } #[cfg(test)] @@ -136,6 +145,13 @@ mod tests { use super::*; use crate::Input; + /// Read and parse bytes as u8 + pub fn read_u8_from_bytes(buf: &[u8], head: &mut usize) -> Result<u8> { + let val = u8::from_le_bytes(buf[*head..*head + 1].try_into()?); + *head += 1; + Ok(val) + } + /// Read and parse bytes as u16 pub fn read_u16_from_bytes(buf: &[u8], head: &mut usize) -> Result<u16> { let val = u16::from_le_bytes(buf[*head..*head + 2].try_into()?); diff --git a/tools/aconfig/src/storage/package_table.rs b/tools/aconfig/src/storage/package_table.rs index 0ce13493b5..40362340e0 100644 --- a/tools/aconfig/src/storage/package_table.rs +++ b/tools/aconfig/src/storage/package_table.rs @@ -277,9 +277,7 @@ mod tests { #[test] // this test point locks down the table serialization fn test_serialization() { - let package_table = create_test_package_table(); - assert!(package_table.is_ok()); - let package_table = package_table.unwrap(); + let package_table = create_test_package_table().unwrap(); let header: &PackageTableHeader = &package_table.header; let reinterpreted_header = PackageTableHeader::from_bytes(&header.as_bytes()); |