blob: 7835cfc7090531ee418106b68204e507d8e3d771 [file] [log] [blame]
#!/usr/bin/env python3
#
# Copyright 2017 Max Ehrlich maxehr@gmail.com
#
# SPDX-License-Identifier: LGPL-2.1-or-later
#
import argparse
import contextlib
import os
import shutil
import subprocess
import tempfile
import time
try:
from jinja2 import Environment, Template
except ImportError:
print(
"Error: jinja2 is required for this script. Install it with: pip install jinja2"
)
exit(1)
@contextlib.contextmanager
def cd(path):
prev_cwd = os.getcwd()
os.chdir(path)
yield
os.chdir(prev_cwd)
# Embedded Jinja2 template for firmware.metainfo.xml
FIRMWARE_METAINFO_TEMPLATE = """<?xml version="1.0" encoding="UTF-8"?>
<component type="firmware">
<id>org.{{ developer_name }}.guid{{ firmware_id }}</id>
<name>{{ firmware_name }}</name>{% if name_variant_suffix %}
<name_variant_suffix>{{ name_variant_suffix }}</name_variant_suffix>{% endif %}
<summary>{{ firmware_summary }}</summary>
<description>
<p>{{ firmware_description }}</p>
</description>
<provides>
<firmware type="flashed">{{ device_guid }}</firmware>
</provides>
<url type="homepage">{{ firmware_homepage }}</url>
<metadata_license>CC0-1.0</metadata_license>
<project_license>proprietary</project_license>
<developer_name>{{ developer_name }}</developer_name>
<releases>
<release urgency="{{ release_urgency }}" version="{{ release_version }}" date="{{ date }}">
<checksum filename="firmware.bin" target="content"/>
<description>
<p>{{ release_description }}</p>
{% for feature in release_features %}
<p>{{ feature }}</p>
{% endfor %}
</description>
</release>
</releases>
<custom>
<value key="LVFS::VersionFormat">{{ version_format }}</value>
<value key="LVFS::UpdateProtocol">{{ update_protocol }}</value>
</custom>
<categories>
{% for category in categories %}
<category>{{ category }}</category>
{% endfor %}
</categories>
</component>"""
def load_template():
"""Load the Jinja2 template from the embedded string with XML autoescape."""
env = Environment(autoescape=True)
return env.from_string(FIRMWARE_METAINFO_TEMPLATE)
def make_firmware_metainfo(firmware_info, dst):
local_info = vars(firmware_info)
local_info["firmware_id"] = local_info["device_guid"][0:8]
# Convert name-variant-suffix to name_variant_suffix for template
if "name_variant_suffix" not in local_info and hasattr(
firmware_info, "name_variant_suffix"
):
local_info["name_variant_suffix"] = getattr(
firmware_info, "name_variant_suffix", ""
)
# Parse release features into a list for template iteration
if "release_features" in local_info and local_info["release_features"]:
features = local_info["release_features"]
# Handle both string (with | delimiter) and list inputs
if isinstance(features, str):
features = features.split("|")
elif isinstance(features, list):
# If it's already a list, use it directly
pass
else:
features = []
# Clean up features and filter out empty ones
cleaned_features = []
for feature in features:
feature = feature.strip()
if feature: # Only add non-empty features
cleaned_features.append(feature)
local_info["release_features"] = cleaned_features
else:
local_info["release_features"] = []
# Parse firmware categories into a list for template iteration
if "firmware_category" in local_info and local_info["firmware_category"]:
categories = local_info["firmware_category"]
# Handle both string (with | delimiter) and list inputs
if isinstance(categories, str):
categories = categories.split("|")
elif isinstance(categories, list):
# Flatten the list in case we have nested lists from argparse append + pipe-separated values
flattened_categories = []
for item in categories:
if isinstance(item, str) and "|" in item:
flattened_categories.extend(item.split("|"))
else:
flattened_categories.append(item)
categories = flattened_categories
else:
categories = ["X-System"] # Default fallback
# Clean up categories and filter out empty ones
cleaned_categories = []
for category in categories:
category = category.strip()
if category: # Only add non-empty categories
cleaned_categories.append(category)
local_info["categories"] = (
cleaned_categories if cleaned_categories else ["X-System"]
)
else:
# Default category if none provided
local_info["categories"] = ["X-System"]
# Load and render the Jinja2 template
template = load_template()
firmware_metainfo = template.render(**local_info, date=time.strftime("%Y-%m-%d"))
with open(os.path.join(dst, "firmware.metainfo.xml"), "w") as f:
f.write(firmware_metainfo)
def extract_exe(exe, dst):
command = ["7z", "x", f"-o{dst}", exe]
subprocess.check_call(command, stdout=subprocess.DEVNULL)
def get_firmware_bin(root, bin_path, dst):
with cd(root):
shutil.copy(bin_path, os.path.join(dst, "firmware.bin"))
def create_firmware_cab(exe, folder):
with cd(folder):
if os.name == "nt":
directive = os.path.join(folder, "directive")
with open(directive, "w") as wfd:
wfd.write(".OPTION EXPLICIT\r\n")
wfd.write(".Set CabinetNameTemplate=firmware.cab\r\n")
wfd.write(".Set DiskDirectory1=.\r\n")
wfd.write("firmware.bin\r\n")
wfd.write("firmware.metainfo.xml\r\n")
command = ["makecab.exe", "/f", directive]
else:
command = [
"fwupdtool",
"build-cabinet",
"firmware.cab",
"firmware.bin",
"firmware.metainfo.xml",
]
subprocess.check_call(command)
def main(args):
with tempfile.TemporaryDirectory() as d:
print(f"Using temp directory {d}")
if args.exe:
print("Extracting firmware exe")
extract_exe(args.exe, d)
print("Locating firmware bin")
get_firmware_bin(d, args.bin, d)
print("Creating metainfo")
make_firmware_metainfo(args, d)
print("Creating cabinet file")
create_firmware_cab(args, d)
print("Done")
shutil.copy(os.path.join(d, "firmware.cab"), args.out)
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Create fwupd packaged from windows executables"
)
parser.add_argument(
"--firmware-name",
help="Name of the firmware package can be customized (e.g. DellTBT)",
required=True,
)
parser.add_argument(
"--name-variant-suffix",
help="Name variant suffix for the firmware package",
default="",
)
parser.add_argument(
"--firmware-summary", help="One line description of the firmware package"
)
parser.add_argument(
"--firmware-description", help="Longer description of the firmware package"
)
parser.add_argument(
"--device-guid",
help="GUID of the device this firmware will run on, this *must* match the output of one of the GUIDs in `fwupdmgr get-devices`",
required=True,
)
parser.add_argument("--firmware-homepage", help="Website for the firmware provider")
parser.add_argument(
"--developer-name", help="Name of the firmware developer", required=True
)
parser.add_argument(
"--release-version",
help="Version number of the firmware package",
required=True,
)
parser.add_argument(
"--version-format",
help="Version format, e.g. quad or triplet",
required=True,
)
parser.add_argument(
"--update-protocol",
help="Update protocol, e.g. org.uefi.capsule",
required=True,
)
parser.add_argument(
"--update-message",
help="Update message for LVFS compliance",
default="This firmware has been tested on target hardware and verified to work correctly",
)
parser.add_argument(
"--release-description", help="Description of the firmware release"
)
parser.add_argument(
"--release-features",
help="Features in the release feature list. Accept multiple calls or one call splitting them with |",
action="append",
default=[],
)
parser.add_argument(
"--release-urgency",
help="Release urgency (e.g. low, medium, high, critical)",
choices=["low", "medium", "high", "critical"],
default="medium",
)
parser.add_argument(
"--firmware-category",
help="Firmware category (e.g. X-System, X-Device, X-EmbeddedController, ...) Split them with | or use multiple --firmware-category arguments",
action="append",
choices=[
"X-System",
"X-Device",
"X-EmbeddedController",
"X-ManagementEngine",
"X-Controller",
"X-CorporateManagementEngine",
"X-ConsumerManagementEngine",
"X-ThunderboltController",
"X-PlatformSecurityProcessor",
"X-CpuMicrocode",
"X-Configuration",
"X-Battery",
"X-Camera",
"X-TPM",
"X-Touchpad",
"X-Mouse",
"X-Keyboard",
"X-StorageController",
"X-NetworkInterface",
"X-VideoDisplay",
"X-BaseboardManagementController",
"X-UsbReceiver",
"X-Drive",
"X-FlashDrive",
"X-SolidStateDrive",
"X-Gpu",
"X-Dock",
"X-UsbDock",
"X-FingerprintReader",
"X-GraphicsTablet",
"X-InputController",
"X-Headphones",
"X-Headset",
],
default=[],
)
parser.add_argument(
"--exe", help="(optional) Executable file to extract firmware from"
)
parser.add_argument(
"--bin",
help="Path to the .bin file (Relative if inside the executable; Absolute if outside) to use as the firmware image",
required=True,
)
parser.add_argument("--out", help="Output cab file path", required=True)
args = parser.parse_args()
main(args)