Skip to content

Commit 00fdafa

Browse files
committed
Merge branch 'dev' into release/1.20.4
2 parents 18dd27c + 8c1a391 commit 00fdafa

File tree

3 files changed

+280
-1
lines changed

3 files changed

+280
-1
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ ffpr
1717
pkcs11.password
1818
app/play
1919
app/huawei
20+
release-creds.toml
2021

2122
!/scripts/drone-static-upload.sh
2223
!/scripts/drone-upload-exists.sh

app/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ android {
8181
multiDexEnabled = true
8282

8383
vectorDrawables.useSupportLibrary = true
84-
project.ext.set("archivesBaseName", "session")
84+
setProperty("archivesBaseName", "session-${versionName}")
8585

8686
buildConfigField "long", "BUILD_TIMESTAMP", getLastCommitTimestamp() + "L"
8787
buildConfigField "String", "GIT_HASH", "\"$getGitHash\""

scripts/build-and-release.py

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
#!/usr/bin/env python3
2+
3+
import subprocess
4+
import json
5+
import os
6+
import sys
7+
import shutil
8+
import re
9+
import tomllib
10+
from dataclasses import dataclass
11+
import tempfile
12+
import base64
13+
import string
14+
import glob
15+
16+
17+
# Number of versions to keep in the fdroid repo. Will remove all the older versions.
18+
KEEP_FDROID_VERSIONS = 4
19+
20+
21+
@dataclass
22+
class BuildResult:
23+
max_version_code: int
24+
version_name: str
25+
apk_paths: list[str]
26+
bundle_path: str
27+
package_id: str
28+
29+
@dataclass
30+
class BuildCredentials:
31+
keystore_b64: str
32+
keystore_password: str
33+
key_alias: str
34+
key_password: str
35+
36+
def __init__(self, credentials: dict):
37+
self.keystore_b64 = credentials['keystore'].strip()
38+
self.keystore_password = credentials['keystore_password']
39+
self.key_alias = credentials['key_alias']
40+
self.key_password = credentials['key_password']
41+
42+
def build_releases(project_root: str, flavor: str, credentials_property_prefix: str, credentials: BuildCredentials, huawei: bool=False) -> BuildResult:
43+
(keystore_fd, keystore_file) = tempfile.mkstemp(prefix='keystore_', suffix='.jks', dir=os.path.join(project_root, 'build'))
44+
try:
45+
with os.fdopen(keystore_fd, 'wb') as f:
46+
f.write(base64.b64decode(credentials.keystore_b64))
47+
48+
gradle_commands = f"""./gradlew \
49+
-P{credentials_property_prefix}_STORE_FILE='{keystore_file}'\
50+
-P{credentials_property_prefix}_STORE_PASSWORD='{credentials.keystore_password}' \
51+
-P{credentials_property_prefix}_KEY_ALIAS='{credentials.key_alias}' \
52+
-P{credentials_property_prefix}_KEY_PASSWORD='{credentials.key_password}'"""
53+
54+
if huawei:
55+
gradle_commands += ' -Phuawei '
56+
57+
subprocess.run(f"""{gradle_commands} \
58+
assemble{flavor.capitalize()}Release \
59+
bundle{flavor.capitalize()}Release --stacktrace""", shell=True, check=True, cwd=project_root)
60+
61+
apk_output_dir = os.path.join(project_root, f'app/build/outputs/apk/{flavor}/release')
62+
63+
with open(os.path.join(apk_output_dir, 'output-metadata.json')) as f:
64+
play_outputs = json.load(f)
65+
66+
apks = [os.path.join(apk_output_dir, f['outputFile']) for f in play_outputs['elements']]
67+
max_version_code = max(map(lambda element: element['versionCode'], play_outputs['elements']))
68+
package_id = play_outputs['applicationId']
69+
version_name = play_outputs['elements'][0]['versionName']
70+
71+
print('Max version code is: ', max_version_code)
72+
73+
return BuildResult(max_version_code=max_version_code,
74+
apk_paths=apks,
75+
package_id=package_id,
76+
version_name=version_name,
77+
bundle_path=os.path.join(project_root, f'app/build/outputs/bundle/{flavor}Release/session-{version_name}-{flavor}-release.aab'))
78+
79+
finally:
80+
print(f'Cleaning up keystore file: {keystore_file}')
81+
os.remove(keystore_file)
82+
83+
84+
project_root = os.path.dirname(sys.path[0])
85+
credentials_file_path = os.path.join(project_root, 'release-creds.toml')
86+
fdroid_repo_path = os.path.join(project_root, 'build/fdroidrepo')
87+
88+
def detect_android_sdk() -> str:
89+
sdk_dir = os.environ.get('ANDROID_HOME')
90+
if sdk_dir is None:
91+
with open(os.path.join(project_root, 'local.properties')) as f:
92+
matched = next(re.finditer(r'^sdk.dir=(.+?)$', f.read(), re.MULTILINE), None)
93+
sdk_dir = matched.group(1) if matched else None
94+
95+
if sdk_dir is None or not os.path.isdir(sdk_dir):
96+
raise Exception('Android SDK not found. Please set ANDROID_HOME or add sdk.dir to local.properties')
97+
98+
return sdk_dir
99+
100+
101+
def update_fdroid(build: BuildResult, fdroid_workspace: str, creds: BuildCredentials):
102+
# Check if there's a git repo at the fdroid repo path by running git status
103+
try:
104+
subprocess.check_call(f'git -C {fdroid_repo_path} status', shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
105+
subprocess.check_call(f'git fetch', shell=True, cwd=fdroid_workspace)
106+
print(f'Found fdroid git repo at {fdroid_repo_path}')
107+
except subprocess.CalledProcessError:
108+
print(f'No fdroid git repo found at {fdroid_repo_path}. Cloning using gh.')
109+
subprocess.run(f'gh repo clone session-foundation/session-fdroid {fdroid_repo_path} -- --depth=1', shell=True, check=True)
110+
111+
# Create a branch for the release
112+
print(f'Creating a branch for the fdroid release: {build.version_name}')
113+
try:
114+
branch_name = f'release/{build.version_name}'
115+
# Clean and switch to master before doing anything
116+
subprocess.check_call(f'git reset --hard HEAD && git checkout master', shell=True, cwd=fdroid_workspace, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
117+
118+
# Delete the existing local branch regardlessly
119+
subprocess.run(f'git branch -D {branch_name}', check=False, shell=True, cwd=fdroid_workspace)
120+
121+
# Check if the remote branch already exists, or we need to create a new one
122+
try:
123+
subprocess.check_call(f'git ls-remote --exit-code origin refs/heads/{branch_name}', shell=True, cwd=fdroid_workspace, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
124+
print(f'Branch {branch_name} already exists. Checking out...')
125+
subprocess.check_call(f'git checkout {branch_name}', shell=True, cwd=fdroid_workspace)
126+
except subprocess.CalledProcessError:
127+
print(f'Branch {branch_name} not found. Creating a new branch.')
128+
subprocess.check_call(f'git checkout -b {branch_name} origin/master', shell=True, cwd=fdroid_workspace)
129+
130+
except subprocess.CalledProcessError:
131+
print(f'Failed to create a branch for the release. ')
132+
sys.exit(1)
133+
134+
# Copy the apks to the fdroid repo
135+
for apk in build.apk_paths:
136+
if apk.endswith('-universal.apk'):
137+
print('Skipping universal apk:', apk)
138+
continue
139+
140+
dst = os.path.join(fdroid_workspace, 'repo/' + os.path.basename(apk))
141+
print('Copying', apk, 'to', dst)
142+
shutil.copy(apk, dst)
143+
144+
# Make sure there are only last three versions of APKs
145+
all_apk_versions_and_ctime = [(re.search(r'session-(.+?)-', os.path.basename(name)).group(1), os.path.getctime(name))
146+
for name in glob.glob(os.path.join(fdroid_workspace, 'repo/session-*-arm64-v8a.apk'))]
147+
# Sort by ctime DESC
148+
all_apk_versions_and_ctime.sort(key=lambda x: x[1], reverse=True)
149+
# Remove all but the last three versions
150+
for version, _ in all_apk_versions_and_ctime[KEEP_FDROID_VERSIONS:]:
151+
for apk in glob.glob(os.path.join(fdroid_workspace, f'repo/session-{version}-*.apk')):
152+
print('Removing old apk:', apk)
153+
os.remove(apk)
154+
155+
# Update the metadata file
156+
metadata_file = os.path.join(fdroid_workspace, f'metadata/{build.package_id}.yml')
157+
with open(f'{metadata_file}.tpl', 'r') as template_file:
158+
metadata_template = string.Template(template_file.read())
159+
metadata_contents = metadata_template.substitute({
160+
'currentVersionCode': build.max_version_code,
161+
})
162+
with open(metadata_file, 'w') as file:
163+
file.write(metadata_contents)
164+
165+
[keystore_fd, keystore_path] = tempfile.mkstemp(prefix='fdroid_keystore_', suffix='.p12', dir=os.path.join(project_root, 'build'))
166+
config_file_path = os.path.join(fdroid_workspace, 'config.yml')
167+
168+
try:
169+
android_sdk = detect_android_sdk()
170+
with os.fdopen(keystore_fd, 'wb') as f:
171+
f.write(base64.b64decode(creds.keystore_b64))
172+
173+
# Read the config template and create a config file
174+
with open(f'{config_file_path}.tpl') as config_template_file:
175+
config_template = string.Template(config_template_file.read())
176+
with open(config_file_path, 'w') as f:
177+
f.write(config_template.substitute({
178+
'keystore_file': keystore_path,
179+
'keystore_pass': creds.keystore_password,
180+
'repo_keyalias': creds.key_alias,
181+
'key_pass': creds.key_password,
182+
'android_sdk': android_sdk
183+
}))
184+
185+
186+
# Run fdroid update
187+
print("Running fdroid update...")
188+
environs = os.environ.copy()
189+
subprocess.run('fdroid update', shell=True, check=True, cwd=fdroid_workspace, env=environs)
190+
finally:
191+
print(f'Cleaning up...')
192+
if os.path.exists(metadata_file):
193+
os.remove(metadata_file)
194+
195+
if os.path.exists(keystore_path):
196+
os.remove(keystore_path)
197+
198+
if os.path.exists(config_file_path):
199+
os.remove(config_file_path)
200+
201+
# Commit the changes
202+
print('Committing the changes...')
203+
subprocess.run(f'git add . && git commit -am "Prepare for release {build.version_name}"', shell=True, check=True, cwd=fdroid_workspace)
204+
205+
# Create Pull Request for releases
206+
print('Creating a pull request...')
207+
subprocess.run(f'''\
208+
gh pr create --base master \
209+
--title "Release {build.version_name}" \
210+
--body "This is an automated release preparation for Release {build.version_name}. Human beings are still required to approve and merge this PR."\
211+
''', shell=True, check=True, cwd=fdroid_workspace)
212+
213+
214+
# Make sure gh command is available
215+
if shutil.which('gh') is None:
216+
print('`gh` command not found. It is required to automate fdroid releases. Please install it from https://cli.github.com/', file=sys.stderr)
217+
sys.exit(1)
218+
219+
# Make sure credentials file exists
220+
if not os.path.isfile(credentials_file_path):
221+
print(f'Credentials file not found at {credentials_file_path}. You should ask the project maintainer for the file.', file=sys.stderr)
222+
sys.exit(1)
223+
224+
with open(credentials_file_path, 'rb') as f:
225+
credentials = tomllib.load(f)
226+
227+
228+
print("Building play releases...")
229+
play_build_result = build_releases(
230+
project_root=project_root,
231+
flavor='play',
232+
credentials=BuildCredentials(credentials['build']['play']),
233+
credentials_property_prefix='SESSION'
234+
)
235+
236+
print("Updating fdroid repo...")
237+
update_fdroid(build=play_build_result, creds=BuildCredentials(credentials['fdroid']), fdroid_workspace=os.path.join(fdroid_repo_path, 'fdroid'))
238+
239+
print("Building huawei releases...")
240+
huawei_build_result = build_releases(
241+
project_root=project_root,
242+
flavor='huawei',
243+
credentials=BuildCredentials(credentials['build']['huawei']),
244+
credentials_property_prefix='SESSION_HUAWEI',
245+
huawei=True
246+
)
247+
248+
# If the a github release draft exists, upload the apks to the release
249+
try:
250+
release_info = json.loads(subprocess.check_output(f'gh release view --json isDraft {play_build_result.version_name}', shell=True, cwd=project_root))
251+
if release_info['draft'] == True:
252+
print(f'Uploading build artifact to the release {play_build_result.version_name} draft...')
253+
files_to_upload = [*play_build_result.apk_paths,
254+
play_build_result.bundle_path,
255+
*huawei_build_result.apk_paths]
256+
upload_commands = ['gh', 'release', 'upload', play_build_result.version_name, '--clobber', *files_to_upload]
257+
subprocess.run(upload_commands, shell=False, cwd=project_root, check=True)
258+
259+
print('Successfully uploaded these files to the draft release: ')
260+
for file in files_to_upload:
261+
print(file)
262+
else:
263+
print(f'Release {play_build_result.version_name} not a draft. Skipping upload of apks to the release.')
264+
except subprocess.CalledProcessError:
265+
print(f'{play_build_result.version_name} has not had a release draft created. Skipping upload of apks to the release.')
266+
267+
268+
print('\n=====================')
269+
print('Build result: ')
270+
print('Play:')
271+
for apk in play_build_result.apk_paths:
272+
print(f'\t{apk}')
273+
print(f'\t{play_build_result.bundle_path}')
274+
275+
print('Huawei:')
276+
for apk in huawei_build_result.apk_paths:
277+
print(f'\t{apk}')
278+
print('=====================')

0 commit comments

Comments
 (0)