Add release management tasks

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
This commit is contained in:
Andrew Gunnerson
2023-08-30 18:47:10 -04:00
parent fb4b93b403
commit 44d62f5147
18 changed files with 705 additions and 176 deletions
+5
View File
@@ -0,0 +1,5 @@
[alias]
xtask = "run --package xtask --"
[env]
CARGO_WORKSPACE_DIR = { value = "", relative = true }
+2 -2
View File
@@ -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
+37
View File
@@ -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
+15
View File
@@ -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
View File
@@ -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
View File
@@ -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"
+1 -1
View File
@@ -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.
+7
View File
@@ -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
View File
@@ -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
-166
View File
@@ -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()
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+24
View File
@@ -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
+32
View File
@@ -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.
+143
View File
@@ -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(())
}
+39
View File
@@ -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,
}
+242
View File
@@ -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,
}
+93
View File
@@ -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,
}