mirror of
https://github.com/chenxiaolong/avbroot.git
synced 2026-06-01 22:18:48 +02:00
Add release management tasks
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
[alias]
|
||||
xtask = "run --package xtask --"
|
||||
|
||||
[env]
|
||||
CARGO_WORKSPACE_DIR = { value = "", relative = true }
|
||||
@@ -24,8 +24,8 @@ jobs:
|
||||
| sed -E "s/^v//g;s/([^-]*-g)/r\1/;s/-/./g" \
|
||||
>> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Build and test
|
||||
run: ./modules/build.py
|
||||
- name: Build modules
|
||||
run: cargo xtask modules -a
|
||||
|
||||
- name: Archive artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
on:
|
||||
push:
|
||||
# Uncomment to test against a branch
|
||||
#branches:
|
||||
# - ci
|
||||
tags:
|
||||
- 'v*'
|
||||
jobs:
|
||||
create_release:
|
||||
name: Create Github release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Get version from tag
|
||||
id: get_version
|
||||
run: |
|
||||
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
|
||||
version=${GITHUB_REF#refs/tags/v}
|
||||
else
|
||||
version=0.0.0.${GITHUB_REF#refs/heads/}
|
||||
fi
|
||||
echo "version=${version}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Create release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag_name: v${{ steps.get_version.outputs.version }}
|
||||
name: Version ${{ steps.get_version.outputs.version }}
|
||||
body_path: RELEASE.md
|
||||
draft: true
|
||||
prerelease: false
|
||||
@@ -0,0 +1,15 @@
|
||||
<!--
|
||||
When adding new changelog entries, use [Issue #0] to link to issues and
|
||||
[PR #0 @user] to link to pull requests. Then run:
|
||||
|
||||
cargo xtask update-changelog
|
||||
|
||||
to update the actual links at the bottom of the file.
|
||||
-->
|
||||
|
||||
### Unreleased
|
||||
|
||||
* Initial Rust release. The old Python implementation can be found in the `python` branch. ([PR #130 @chenxiaolong])
|
||||
|
||||
<!-- Do not manually edit the lines below. Use `cargo xtask update-changelog` to regenerate. -->
|
||||
[PR #130 @chenxiaolong]: https://github.com/chenxiaolong/avbroot/pull/130
|
||||
Generated
+48
@@ -777,6 +777,12 @@ version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "human-sort"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "140a09c9305e6d5e557e2ed7cbc68e05765a7d4213975b87cb04920689cc6219"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "0.14.27"
|
||||
@@ -1521,6 +1527,15 @@ dependencies = [
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schannel"
|
||||
version = "0.1.22"
|
||||
@@ -1993,6 +2008,16 @@ version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "want"
|
||||
version = "0.3.1"
|
||||
@@ -2113,6 +2138,15 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
@@ -2217,6 +2251,20 @@ dependencies = [
|
||||
"spki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"human-sort",
|
||||
"regex",
|
||||
"tempfile",
|
||||
"toml_edit",
|
||||
"walkdir",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xz2"
|
||||
version = "0.1.7"
|
||||
|
||||
+8
-1
@@ -3,6 +3,7 @@ name = "avbroot"
|
||||
version = "0.1.0"
|
||||
license = "GPL-3.0-only"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/chenxiaolong/avbroot"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -74,4 +75,10 @@ assert_matches = "1.5.0"
|
||||
static = ["bzip2/static", "xz2/static"]
|
||||
|
||||
[workspace]
|
||||
members = ["e2e"]
|
||||
members = ["e2e", "xtask"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.0"
|
||||
license = "GPL-3.0-only"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/chenxiaolong/avbroot"
|
||||
|
||||
@@ -142,7 +142,7 @@ To update Android or Magisk:
|
||||
avbroot's Magisk modules can be found on the [releases page](https://github.com/chenxiaolong/avbroot/releases) or they can be built locally by running:
|
||||
|
||||
```bash
|
||||
python modules/build.py
|
||||
cargo xtask modules -a
|
||||
```
|
||||
|
||||
This requires Java and the Android SDK to be installed. The `ANDROID_HOME` environment variable should be set to the Android SDK path.
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
The changelog can be found at: [`CHANGELOG.md`](./CHANGELOG.md).
|
||||
|
||||
---
|
||||
|
||||
See [`README.md`](./README.md) for information on how to use avbroot.
|
||||
|
||||
The downloads are digitally signed. Please consider [verifying the digital signatures](./README.md#verifying-digital-signatures) of the binaries (or building from source) since avbroot is an application with access to your OTA/AVB signing keys.
|
||||
+5
-2
@@ -1,7 +1,10 @@
|
||||
[package]
|
||||
name = "e2e"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
version.workspace = true
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
repository.workspace = true
|
||||
publish = false
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
|
||||
@@ -1,166 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import zipfile
|
||||
|
||||
|
||||
def natsort_key(text, regex=re.compile(r'(\d+)')):
|
||||
return [int(s) if s.isdigit() else s for s in regex.split(text)]
|
||||
|
||||
|
||||
def newest_child_by_name(directory):
|
||||
children = os.listdir(directory)
|
||||
if not children:
|
||||
raise ValueError(f'{directory} has no children')
|
||||
|
||||
child = sorted(children, key=natsort_key)[-1]
|
||||
return os.path.join(directory, child)
|
||||
|
||||
|
||||
def build_empty_zip():
|
||||
stream = io.BytesIO()
|
||||
|
||||
with zipfile.ZipFile(stream, 'w'):
|
||||
pass
|
||||
|
||||
return stream.getvalue()
|
||||
|
||||
|
||||
def build_dex(sources):
|
||||
if 'ANDROID_HOME' not in os.environ:
|
||||
raise ValueError('ANDROID_HOME must be set to the Android SDK path')
|
||||
|
||||
sdk = os.environ['ANDROID_HOME']
|
||||
build_tools = newest_child_by_name(os.path.join(sdk, 'build-tools'))
|
||||
platform = newest_child_by_name(os.path.join(sdk, 'platforms'))
|
||||
d8 = os.path.join(build_tools, 'd8')
|
||||
if os.name == 'nt':
|
||||
d8 += '.bat'
|
||||
android_jar = os.path.join(platform, 'android.jar')
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
subprocess.check_call([
|
||||
'javac',
|
||||
'-source', '1.8',
|
||||
'-target', '1.8',
|
||||
'-cp', android_jar,
|
||||
'-d', temp_dir,
|
||||
*sources,
|
||||
])
|
||||
|
||||
class_files = []
|
||||
for root, _, files in os.walk(temp_dir):
|
||||
for f in files:
|
||||
if f.endswith('.class'):
|
||||
class_files.append(os.path.join(root, f))
|
||||
|
||||
subprocess.check_call([
|
||||
d8,
|
||||
'--output', temp_dir,
|
||||
*class_files,
|
||||
])
|
||||
|
||||
with open(os.path.join(temp_dir, 'classes.dex'), 'rb') as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def parse_props(raw_prop):
|
||||
result = {}
|
||||
|
||||
for line in raw_prop.decode('UTF-8').splitlines():
|
||||
k, delim, v = line.partition('=')
|
||||
if not delim:
|
||||
raise ValueError(f'Malformed line: {repr(line)}')
|
||||
|
||||
result[k.strip()] = v.strip()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def build_module(dist_dir, common_dir, module_dir, extra_files):
|
||||
with open(os.path.join(module_dir, 'module.prop'), 'rb') as f:
|
||||
module_prop_raw = f.read()
|
||||
module_prop = parse_props(module_prop_raw)
|
||||
|
||||
name = module_prop['name']
|
||||
version = module_prop['version'].removeprefix('v')
|
||||
zip_path = os.path.join(dist_dir, f'{name}-{version}.zip')
|
||||
|
||||
with zipfile.ZipFile(zip_path, 'w') as z:
|
||||
file_map = {
|
||||
'META-INF/com/google/android/update-binary': {
|
||||
'file': os.path.join(common_dir, 'update-binary'),
|
||||
},
|
||||
'META-INF/com/google/android/updater-script': {
|
||||
'file': os.path.join(common_dir, 'updater-script'),
|
||||
},
|
||||
'module.prop': {
|
||||
'data': module_prop_raw,
|
||||
},
|
||||
**extra_files,
|
||||
}
|
||||
|
||||
for name, source in sorted(file_map.items()):
|
||||
# Build our own ZipInfo to ensure archive is reproducible
|
||||
info = zipfile.ZipInfo(name)
|
||||
with z.open(info, 'w') as f_out:
|
||||
if 'data' in source:
|
||||
f_out.write(source['data'])
|
||||
else:
|
||||
with open(source['file'], 'rb') as f_in:
|
||||
shutil.copyfileobj(f_in, f_out)
|
||||
|
||||
return zip_path
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('module', nargs='*',
|
||||
default=('clearotacerts', 'oemunlockonboot'),
|
||||
help='Module to build')
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
|
||||
dist_dir = os.path.join(sys.path[0], 'dist')
|
||||
os.makedirs(dist_dir, exist_ok=True)
|
||||
|
||||
common_dir = os.path.join(sys.path[0], 'common')
|
||||
|
||||
for module in args.module:
|
||||
module_dir = os.path.join(sys.path[0], module)
|
||||
|
||||
if module == 'clearotacerts':
|
||||
extra_files = {
|
||||
'system/etc/security/otacerts.zip': {
|
||||
'data': build_empty_zip(),
|
||||
},
|
||||
}
|
||||
elif module == 'oemunlockonboot':
|
||||
extra_files = {
|
||||
'classes.dex': {
|
||||
'data': build_dex([os.path.join(module_dir, 'Main.java')]),
|
||||
},
|
||||
'service.sh': {
|
||||
'file': os.path.join(module_dir, 'service.sh'),
|
||||
},
|
||||
}
|
||||
else:
|
||||
raise ValueError(f'Invalid module: {module}')
|
||||
|
||||
module_zip = build_module(dist_dir, common_dir, module_dir, extra_files)
|
||||
print('Built module', module_zip)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,6 +1,6 @@
|
||||
id=com.chiller3.avbroot.clearotacerts
|
||||
name=clearotacerts
|
||||
version=v1.0
|
||||
versionCode=1
|
||||
version=v0.1.0
|
||||
versionCode=256
|
||||
author=chenxiaolong
|
||||
description=Block A/B OTAs by clearing verification certificates
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
id=com.chiller3.avbroot.oemunlockonboot
|
||||
name=oemunlockonboot
|
||||
version=v1.0
|
||||
versionCode=1
|
||||
version=v0.1.0
|
||||
versionCode=256
|
||||
author=chenxiaolong
|
||||
description=Enable OEM unlocking on every boot
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "xtask"
|
||||
version.workspace = true
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
repository.workspace = true
|
||||
publish = false
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.75"
|
||||
clap = { version = "4.4.1", features = ["derive"] }
|
||||
human-sort = "0.2.2"
|
||||
regex = { version = "1.9.4", default-features = false, features = ["perf", "std"] }
|
||||
tempfile = "3.8.0"
|
||||
toml_edit = "0.19.14"
|
||||
walkdir = "2.3.3"
|
||||
|
||||
# https://github.com/zip-rs/zip/pull/383
|
||||
[dependencies.zip]
|
||||
git = "https://github.com/chenxiaolong/zip"
|
||||
rev = "989101f9384b9e94e36e6e9e0f51908fdf98bde6"
|
||||
default-features = false
|
||||
@@ -0,0 +1,32 @@
|
||||
# Release tools
|
||||
|
||||
## Updating the project version
|
||||
|
||||
To update the project version, run:
|
||||
|
||||
```bash
|
||||
cargo xtask set-version -V <version>
|
||||
```
|
||||
|
||||
This will update the main [`Cargo.toml`](../Cargo.toml) and each module's `module.prop` file.
|
||||
|
||||
## Updating changelog links
|
||||
|
||||
To add an entry to [`CHANGELOG.md`](../CHANGELOG.md), the user should manually type in a message and add repository references, like `[Issue #0]` or `[PR #0 @user]`.
|
||||
|
||||
Then, run the following command to generate the appropriate link references:
|
||||
|
||||
```bash
|
||||
cargo xtask update-changelog
|
||||
```
|
||||
|
||||
## Build modules
|
||||
|
||||
To build avbroot's companion modules, run:
|
||||
|
||||
```bash
|
||||
cargo xtask modules -a
|
||||
# or -m <module1> -m <module2> ...
|
||||
```
|
||||
|
||||
See the main [`README.md`](../README.md#avbroot-magisk-modules) for more details.
|
||||
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
fmt,
|
||||
fs::{self, File},
|
||||
io::{BufRead, BufReader},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use regex::Regex;
|
||||
|
||||
use crate::WORKSPACE_DIR;
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
struct LinkRef {
|
||||
link_type: String,
|
||||
number: u32,
|
||||
user: Option<String>,
|
||||
}
|
||||
|
||||
impl fmt::Display for LinkRef {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "[{} #{}", self.link_type, self.number)?;
|
||||
if let Some(u) = &self.user {
|
||||
write!(f, " @{}", u)?;
|
||||
}
|
||||
write!(f, "]")
|
||||
}
|
||||
}
|
||||
|
||||
fn check_brackets(line: &str) -> Result<()> {
|
||||
let mut expect_opening = true;
|
||||
|
||||
for c in line.chars() {
|
||||
if c == '[' || c == ']' {
|
||||
if (c == '[') != expect_opening {
|
||||
bail!("Mismatched brackets: {line:?}");
|
||||
}
|
||||
|
||||
expect_opening = !expect_opening;
|
||||
}
|
||||
}
|
||||
|
||||
if !expect_opening {
|
||||
bail!("Missing closing bracket: {line:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_changelog_links(path: &Path, base_url: &str) -> Result<()> {
|
||||
let re_standalone_link = Regex::new(r"\[([^\]]+)\]($|[^\(\[])")?;
|
||||
let re_auto_link = Regex::new(r"^(Issue|PR) #([0-9]+)(?: @([a-zA-Z0-9\-]+))?$")?;
|
||||
let mut links = BTreeMap::<LinkRef, String>::new();
|
||||
|
||||
let raw_reader = File::open(path)?;
|
||||
let mut reader = BufReader::new(raw_reader);
|
||||
let mut result = String::new();
|
||||
let mut line = String::new();
|
||||
let mut skip_remaining = false;
|
||||
|
||||
loop {
|
||||
line.clear();
|
||||
|
||||
let n = reader.read_line(&mut line)?;
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
let line = line.trim_end();
|
||||
|
||||
if !skip_remaining {
|
||||
check_brackets(line)?;
|
||||
for link_captures in re_standalone_link.captures_iter(line) {
|
||||
let link_text = link_captures.get(1).unwrap();
|
||||
let captures = re_auto_link
|
||||
.captures(link_text.as_str())
|
||||
.ok_or_else(|| anyhow!("Invalid link format: {link_text:?}"))?;
|
||||
|
||||
let link_ref = captures.get(0).unwrap().as_str();
|
||||
let link_type = captures.get(1).unwrap().as_str();
|
||||
let number: u32 = captures.get(2).unwrap().as_str().parse()?;
|
||||
let user = captures.get(3).map(|c| c.as_str());
|
||||
|
||||
let link = match link_type {
|
||||
"Issue" => {
|
||||
if user.is_some() {
|
||||
bail!("{link_ref} should not have a username");
|
||||
}
|
||||
format!("{base_url}/issues/{number}")
|
||||
}
|
||||
"PR" => {
|
||||
if user.is_none() {
|
||||
bail!("{link_ref} should have a username");
|
||||
}
|
||||
format!("{base_url}/pull/{number}")
|
||||
}
|
||||
t => bail!("Unknown link type: {t:?}"),
|
||||
};
|
||||
|
||||
// #0 is used for examples only.
|
||||
if number != 0 {
|
||||
links.insert(
|
||||
LinkRef {
|
||||
link_type: link_type.to_owned(),
|
||||
number,
|
||||
user: user.map(|u| u.to_owned()),
|
||||
},
|
||||
link,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if line.contains("Do not manually edit the lines below") {
|
||||
skip_remaining = true;
|
||||
}
|
||||
|
||||
result.push_str(line);
|
||||
result.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
for (link_ref, link) in links {
|
||||
result.push_str(&format!("{link_ref}: {link}\n"));
|
||||
}
|
||||
|
||||
fs::write(path, result)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update_changelog_subcommand() -> Result<()> {
|
||||
let path = Path::new(WORKSPACE_DIR).join("CHANGELOG.md");
|
||||
|
||||
update_changelog_links(&path, env!("CARGO_PKG_REPOSITORY"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
mod changelog;
|
||||
mod module;
|
||||
mod version;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
use crate::{module::ModulesCli, version::SetVersionCli};
|
||||
|
||||
const WORKSPACE_DIR: &str = env!("CARGO_WORKSPACE_DIR");
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
match cli.command {
|
||||
Command::SetVersion(c) => version::set_version_subcommand(&c),
|
||||
Command::Modules(c) => module::modules_subcommand(&c),
|
||||
Command::UpdateChangelog => changelog::update_changelog_subcommand(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum Command {
|
||||
SetVersion(SetVersionCli),
|
||||
Modules(ModulesCli),
|
||||
/// Update links in CHANGELOG.md.
|
||||
UpdateChangelog,
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct Cli {
|
||||
#[command(subcommand)]
|
||||
pub command: Command,
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
env,
|
||||
ffi::OsStr,
|
||||
fmt,
|
||||
fs::{self, File},
|
||||
io::{self, BufWriter, Write},
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use clap::{Args, Parser, ValueEnum};
|
||||
use tempfile::TempDir;
|
||||
use walkdir::WalkDir;
|
||||
use zip::{write::FileOptions, ZipWriter};
|
||||
|
||||
use crate::WORKSPACE_DIR;
|
||||
|
||||
#[cfg(unix)]
|
||||
const D8: &str = "d8";
|
||||
#[cfg(windows)]
|
||||
const D8: &str = "d8.bat";
|
||||
|
||||
fn newest_child_by_name(directory: &Path) -> Result<PathBuf> {
|
||||
let mut children = vec![];
|
||||
|
||||
for entry in fs::read_dir(directory)? {
|
||||
let entry = entry?;
|
||||
let path = entry
|
||||
.path()
|
||||
.into_os_string()
|
||||
.into_string()
|
||||
.map_err(|e| anyhow!("Non-UTF-8 path: {e:?}"))?;
|
||||
|
||||
children.push(path);
|
||||
}
|
||||
|
||||
children.sort_by(|a, b| human_sort::compare(a, b));
|
||||
|
||||
children
|
||||
.pop()
|
||||
.map(PathBuf::from)
|
||||
.ok_or_else(|| anyhow!("{directory:?} has no children"))
|
||||
}
|
||||
|
||||
fn build_empty_zip(writer: &mut dyn Write) -> Result<()> {
|
||||
let mut writer = ZipWriter::new_streaming(writer);
|
||||
writer.finish()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_dex(writer: &mut dyn Write, sources: &[&Path]) -> Result<()> {
|
||||
let sdk = env::var_os("ANDROID_HOME")
|
||||
.map(PathBuf::from)
|
||||
.ok_or_else(|| anyhow!("ANDROID_HOME must be set to the Android SDK path"))?;
|
||||
let build_tools = newest_child_by_name(&sdk.join("build-tools"))?;
|
||||
let platform = newest_child_by_name(&sdk.join("platforms"))?;
|
||||
let d8 = build_tools.join(D8);
|
||||
let android_jar = platform.join("android.jar");
|
||||
|
||||
let temp_dir = TempDir::new()?;
|
||||
|
||||
let mut process = Command::new("javac")
|
||||
.args(["-source", "1.8"])
|
||||
.args(["-target", "1.8"])
|
||||
.arg("-cp")
|
||||
.arg(android_jar)
|
||||
.arg("-d")
|
||||
.arg(temp_dir.path())
|
||||
.args(sources)
|
||||
.spawn()?;
|
||||
let status = process.wait()?;
|
||||
if !status.success() {
|
||||
bail!("javac failed with {status}");
|
||||
}
|
||||
|
||||
let mut class_files = vec![];
|
||||
|
||||
for entry in WalkDir::new(temp_dir.path()) {
|
||||
let entry = entry?;
|
||||
|
||||
if entry.path().extension() == Some(OsStr::new("class")) {
|
||||
class_files.push(entry.into_path());
|
||||
}
|
||||
}
|
||||
|
||||
let mut process = Command::new(d8)
|
||||
.arg("--output")
|
||||
.arg(temp_dir.path())
|
||||
.args(class_files)
|
||||
.spawn()?;
|
||||
let status = process.wait()?;
|
||||
if !status.success() {
|
||||
bail!("d8 failed with {status}");
|
||||
}
|
||||
|
||||
let mut reader = File::open(temp_dir.path().join("classes.dex"))?;
|
||||
io::copy(&mut reader, writer)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_props(data: &str) -> Result<HashMap<String, String>> {
|
||||
let mut result = HashMap::new();
|
||||
|
||||
for line in data.split('\n') {
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some((k, v)) = line.split_once('=') else {
|
||||
bail!("Malformed line: {line:?}");
|
||||
};
|
||||
|
||||
result.insert(k.trim().to_owned(), v.trim().to_owned());
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn start_module(
|
||||
dist_dir: &Path,
|
||||
common_dir: &Path,
|
||||
module_dir: &Path,
|
||||
) -> Result<(PathBuf, ZipWriter<BufWriter<File>>)> {
|
||||
let module_prop_raw = fs::read_to_string(module_dir.join("module.prop"))?;
|
||||
let module_prop = parse_props(&module_prop_raw)?;
|
||||
|
||||
let name = module_prop["name"].as_str();
|
||||
let version = module_prop["version"].as_str();
|
||||
let version = version.strip_prefix('v').unwrap_or(version);
|
||||
let zip_path = dist_dir.join(format!("{name}-{version}.zip"));
|
||||
|
||||
let raw_writer = File::create(&zip_path)?;
|
||||
let mut zip_writer = ZipWriter::new(BufWriter::new(raw_writer));
|
||||
|
||||
zip_writer.start_file(
|
||||
"META-INF/com/google/android/update-binary",
|
||||
FileOptions::default(),
|
||||
)?;
|
||||
io::copy(
|
||||
&mut File::open(common_dir.join("update-binary"))?,
|
||||
&mut zip_writer,
|
||||
)?;
|
||||
|
||||
zip_writer.start_file(
|
||||
"META-INF/com/google/android/updater-script",
|
||||
FileOptions::default(),
|
||||
)?;
|
||||
io::copy(
|
||||
&mut File::open(common_dir.join("updater-script"))?,
|
||||
&mut zip_writer,
|
||||
)?;
|
||||
|
||||
zip_writer.start_file("module.prop", FileOptions::default())?;
|
||||
zip_writer.write_all(module_prop_raw.as_bytes())?;
|
||||
|
||||
Ok((zip_path, zip_writer))
|
||||
}
|
||||
|
||||
pub fn modules_subcommand(cli: &ModulesCli) -> Result<()> {
|
||||
let modules_dir = Path::new(WORKSPACE_DIR).join("modules");
|
||||
let common_dir = modules_dir.join("common");
|
||||
let dist_dir = modules_dir.join("dist");
|
||||
|
||||
fs::create_dir_all(&dist_dir)?;
|
||||
|
||||
let modules = if cli.module.all {
|
||||
Module::value_variants()
|
||||
} else {
|
||||
&cli.module.module
|
||||
};
|
||||
|
||||
for module in modules {
|
||||
let module_dir = modules_dir.join(module.to_string());
|
||||
|
||||
let (path, mut writer) = start_module(&dist_dir, &common_dir, &module_dir)?;
|
||||
|
||||
match module {
|
||||
Module::ClearOtaCerts => {
|
||||
writer.start_file("system/etc/security/otacerts.zip", FileOptions::default())?;
|
||||
build_empty_zip(&mut writer)?;
|
||||
}
|
||||
Module::OemUnlockOnBoot => {
|
||||
writer.start_file("classes.dex", FileOptions::default())?;
|
||||
build_dex(&mut writer, &[&module_dir.join("Main.java")])?;
|
||||
|
||||
writer.start_file("service.sh", FileOptions::default())?;
|
||||
let mut reader = File::open(module_dir.join("service.sh"))?;
|
||||
io::copy(&mut reader, &mut writer)?;
|
||||
}
|
||||
}
|
||||
|
||||
writer.finish()?;
|
||||
|
||||
let path = path.canonicalize()?;
|
||||
println!("Built module: {path:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, ValueEnum)]
|
||||
#[value(rename_all = "lower")]
|
||||
enum Module {
|
||||
ClearOtaCerts,
|
||||
OemUnlockOnBoot,
|
||||
}
|
||||
|
||||
impl fmt::Display for Module {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.to_possible_value()
|
||||
.expect("no values are skipped")
|
||||
.get_name()
|
||||
.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
#[group(required = true, multiple = false)]
|
||||
pub struct ModuleGroup {
|
||||
/// Name of module.
|
||||
#[arg(short, long)]
|
||||
module: Vec<Module>,
|
||||
|
||||
/// Build all modules.
|
||||
#[arg(short, long, conflicts_with = "module")]
|
||||
all: bool,
|
||||
}
|
||||
|
||||
/// Build companion modules.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct ModulesCli {
|
||||
#[command(flatten)]
|
||||
module: ModuleGroup,
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
io::{BufRead, BufReader},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use toml_edit::{value, Document};
|
||||
|
||||
use crate::WORKSPACE_DIR;
|
||||
|
||||
fn update_cargo_version(version: &str) -> Result<()> {
|
||||
let path = Path::new(WORKSPACE_DIR).join("Cargo.toml");
|
||||
let data = fs::read_to_string(&path)?;
|
||||
|
||||
let mut document: Document = data.parse()?;
|
||||
document["package"]["version"] = value(version.clone());
|
||||
document["workspace"]["package"]["version"] = value(version.clone());
|
||||
|
||||
fs::write(path, document.to_string())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_module_version(path: &Path, version: &str) -> Result<()> {
|
||||
let mut version_code = 0;
|
||||
|
||||
// 8 bits per version component.
|
||||
for piece in version.split('.') {
|
||||
let piece: u32 = piece.parse()?;
|
||||
version_code <<= 8;
|
||||
version_code |= piece;
|
||||
}
|
||||
|
||||
let raw_reader = File::open(path)?;
|
||||
let mut reader = BufReader::new(raw_reader);
|
||||
let mut result = String::new();
|
||||
let mut line = String::new();
|
||||
|
||||
loop {
|
||||
line.clear();
|
||||
|
||||
let n = reader.read_line(&mut line)?;
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
if line.starts_with("version=") {
|
||||
result.push_str(&format!("version=v{version}\n"));
|
||||
} else if line.starts_with("versionCode=") {
|
||||
result.push_str(&format!("versionCode={version_code}\n"));
|
||||
} else {
|
||||
result.push_str(&line);
|
||||
}
|
||||
}
|
||||
|
||||
fs::write(path, &result)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_version_subcommand(cli: &SetVersionCli) -> Result<()> {
|
||||
update_cargo_version(&cli.version)?;
|
||||
|
||||
let modules_dir = Path::new(WORKSPACE_DIR).join("modules");
|
||||
|
||||
for entry in fs::read_dir(modules_dir)? {
|
||||
let entry = entry?;
|
||||
|
||||
if entry.file_type()?.is_dir() {
|
||||
let module_prop = entry.path().join("module.prop");
|
||||
if module_prop.exists() {
|
||||
update_module_version(&module_prop, &cli.version)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set the version number in all Cargo.toml files and in the module metadata.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct SetVersionCli {
|
||||
/// Version number.
|
||||
#[arg(short = 'V', long, value_name = "VERSION")]
|
||||
version: String,
|
||||
}
|
||||
Reference in New Issue
Block a user