Support interactive installation from a staged release bundle

This commit is contained in:
toorux committed 2026-10-20 21:30:17 +08:00
1 parent 4b06a59995
commit 4e6e538b23
4 files changed
+87 -14

No files matched your search

+2
View File
@@ -60,6 +60,8 @@ python3 bootstrap.py install --archive "./framely-<version-and-build>-offline-li
Angle brackets are placeholders; substitute the actual filename. The engine verifies, stages and extracts under `/home`, then checks internal hashes, avoiding the device's limited `/tmp`.
To walk through the interactive installer with a staged archive, place `install.sh`, `bootstrap.py`, the archive, and its `SHA256SUMS` in one directory, then run `bash install.sh --local-bundle /absolute/path/to/directory`. The wizard still checks the selected channel's latest release tag; the local archive must belong to that tag. After confirmation, the normal engine verifies and installs the complete archive.
## Release packages and CEF
Each device release provides three archives:
+2
View File
@@ -66,6 +66,8 @@ python3 bootstrap.py install --archive "./framely-<版本及构建号>-offline-l
尖括号是占位符,执行前替换为真实文件名。引擎校验后在 `/home` 暂存、解压并检查包内哈希,避免耗尽设备的 `/tmp`。
若要用已暂存的完整安装包测试交互向导,将 `install.sh`、`bootstrap.py`、安装包和对应的 `SHA256SUMS` 放在同一目录,运行 `bash install.sh --local-bundle /目录的绝对路径`。向导仍会查询所选渠道的最新标签,本地包必须属于该标签;最终确认后由正常安装引擎校验并安装整包。
## 发行包与 CEF
每个本体 Release 提供三种压缩包:
+47 -3
View File
@@ -10,7 +10,20 @@ trap 'rm -rf -- "$work"' EXIT
release_path=latest/download
args=("$@")
github_proxy=${FRAMELY_GITHUB_PROXY:-}
local_bundle=''
if (( $# == 2 )) && [[ $1 == --local-bundle ]]; then
local_bundle=$2
[[ $local_bundle == /* && -f $local_bundle/bootstrap.py && -f $local_bundle/SHA256SUMS ]] || {
echo 'Framely: --local-bundle needs an absolute directory containing bootstrap.py and SHA256SUMS.' >&2
exit 1
}
args=()
fi
fetch_engine() {
if [[ -n $local_bundle ]]; then
cp -- "$local_bundle/bootstrap.py" "$work/bootstrap.py"
return
fi
local bootstrap_url="https://github.com/$repo/releases/$release_path/bootstrap.py"
if [[ -n $github_proxy ]]; then
echo "Framely[$FRAMELY_INSTALL_TRACE_ID]: Fetching installer engine through configured GitHub proxy (address hidden)."
@@ -44,7 +57,7 @@ else
fi
fi
}
if (( $# == 0 )); then
if (( ${#args[@]} == 0 )); then
if ! exec 3<>/dev/tty; then
echo 'Framely: The interactive installer needs a terminal. Run it in a Frame terminal, use an explicit command such as install --yes, or add -t to SSH.' >&2
exit 1
@@ -74,6 +87,7 @@ if (( $# == 0 )); then
echo ' FRAMELY INSTALL & MAINTAIN' >&3
echo ' ──────────────────────────────' >&3
echo " Current version: $installed_version" >&3
[[ -z $local_bundle ]] || echo ' Package source: local bundle' >&3
echo >&3
step=menu
while :; do
@@ -167,6 +181,31 @@ if (( $# == 0 )); then
fi
selected_tag=$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["tag"])' "$selection")
selected_size=$(python3 -c 'import json,sys; print(json.loads(sys.argv[1])["size"])' "$selection")
if [[ -n $local_bundle ]]; then
local_archives=()
for candidate in "$local_bundle"/framely-*-linux-arm64.tar.gz; do
[[ -f $candidate ]] || continue
if [[ $wizard_action == install && $candidate == *-offline-linux-arm64.tar.gz ]] ||
[[ $wizard_action == update && $candidate != *-offline-linux-arm64.tar.gz ]]; then
local_archives+=("$candidate")
fi
done
if (( ${#local_archives[@]} != 1 )); then
echo ' The local bundle must contain exactly one archive for this operation.' >&3
step=lookup_error
continue
fi
local_archive=${local_archives[0]}
local_name=${local_archive##*/}
local_suffix='-linux-arm64.tar.gz'
[[ $wizard_action != install ]] || local_suffix='-offline-linux-arm64.tar.gz'
if [[ ! $local_name =~ ^framely-${selected_tag#v}-[a-f0-9]{12}${local_suffix//./\.}$ ]]; then
echo ' The local archive does not match the selected release tag.' >&3
step=lookup_error
continue
fi
selected_size=$(stat -c %s -- "$local_archive")
fi
step=confirm ;;
lookup_error)
printf ' r) Retry b) Back to download route q) Quit: ' >&3
@@ -183,11 +222,16 @@ if (( $# == 0 )); then
echo " Current version: $installed_version" >&3
echo " Target release: $selected_tag" >&3
echo " Package size: about $(( (selected_size + 1048575) / 1048576 )) MiB" >&3
echo " Download route: $([[ -n $github_proxy ]] && echo 'GitHub mirror (address hidden)' || echo 'Direct from GitHub')" >&3
if [[ -n $local_bundle ]]; then
echo ' Package source: Local staged archive (SHA256 checked before installation)' >&3
echo " Release lookup: $([[ -n $github_proxy ]] && echo 'GitHub mirror (address hidden)' || echo 'Direct from GitHub')" >&3
else
echo " Download route: $([[ -n $github_proxy ]] && echo 'GitHub mirror (address hidden)' || echo 'Direct from GitHub')" >&3
fi
printf ' 1) Start b) Back q) Quit: ' >&3
IFS= read -r choice <&3 || exit 1
case $choice in
1) args=("$wizard_action" --version "$selected_tag" --yes); [[ -n $github_proxy ]] && args+=(--github-proxy "$github_proxy"); release_path="download/$selected_tag"; break ;;
1) args=("$wizard_action" --version "$selected_tag" --yes); [[ -n $github_proxy ]] && args+=(--github-proxy "$github_proxy"); [[ -z $local_bundle ]] || args+=(--archive "$local_archive" --checksums "$local_bundle/SHA256SUMS"); release_path="download/$selected_tag"; break ;;
b|B) [[ -n $github_proxy ]] && step=mirror_address || step=mirror ;;
q|Q) exit 0 ;;
esac ;;
+36 -11
View File
@@ -21,7 +21,7 @@ class InstallWizardTests(unittest.TestCase):
self.assertNotEqual(result.returncode, 0)
self.assertIn('interactive installer needs a terminal', result.stderr)
def run_wizard(self, answers, installed=False, damaged=False):
def run_wizard(self, answers, installed=False, damaged=False, local_bundle=False):
with tempfile.TemporaryDirectory() as temp:
root = pathlib.Path(temp)
state = root / 'state'
@@ -44,14 +44,7 @@ class InstallWizardTests(unittest.TestCase):
.replace('/home/.framely/releases', str(releases))
.replace('/home/.framely/repair.sh', str(root / 'repair.sh'))
.replace('/var/lib/framely/current', str(legacy)))
curl = root / 'curl'
curl.write_text('''#!/usr/bin/env python3
import json, os, pathlib, sys
args=sys.argv[1:]
with open(os.environ['WIZARD_LOG'], 'a') as out:
out.write(json.dumps({'url':args[-1], 'proxy':os.environ.get('FRAMELY_GITHUB_PROXY',''),
'fallback':os.environ.get('FRAMELY_GITHUB_AUTO_FALLBACK','')})+'\\n')
pathlib.Path(args[args.index('--output')+1]).write_text(''' + repr('''
engine_source = '''
import json, os, pathlib, sys
args=sys.argv[1:]
with open(os.environ['WIZARD_LOG'], 'a') as out:
@@ -59,9 +52,24 @@ with open(os.environ['WIZARD_LOG'], 'a') as out:
'fallback':os.environ.get('FRAMELY_GITHUB_AUTO_FALLBACK','')})+'\\n')
if '--resolve-release' in args:
print(json.dumps({'tag':'v0.9.0-preview.10','size':1048576}))
''') + ''')
'''
curl = root / 'curl'
curl.write_text('''#!/usr/bin/env python3
import json, os, pathlib, sys
args=sys.argv[1:]
with open(os.environ['WIZARD_LOG'], 'a') as out:
out.write(json.dumps({'url':args[-1], 'proxy':os.environ.get('FRAMELY_GITHUB_PROXY',''),
'fallback':os.environ.get('FRAMELY_GITHUB_AUTO_FALLBACK','')})+'\\n')
pathlib.Path(args[args.index('--output')+1]).write_text(''' + repr(engine_source) + ''')
''')
curl.chmod(0o755)
bundle = root / 'bundle'
if local_bundle:
bundle.mkdir()
(bundle / 'bootstrap.py').write_text(engine_source)
(bundle / 'SHA256SUMS').write_text('test\n')
suffix = '-linux-arm64.tar.gz' if installed else '-offline-linux-arm64.tar.gz'
(bundle / ('framely-0.9.0-preview.10-012345abcdef' + suffix)).write_bytes(b'archive')
sudo = root / 'sudo'
sudo.write_text('#!/bin/sh\nexec "$@"\n')
sudo.chmod(0o755)
@@ -76,7 +84,10 @@ if '--resolve-release' in args:
fcntl.ioctl(slave, termios.TIOCSCTTY, 0)
try:
process = subprocess.Popen(['bash', str(script)], stdin=slave, stdout=slave,
command = ['bash', str(script)]
if local_bundle:
command += ['--local-bundle', str(bundle)]
process = subprocess.Popen(command, stdin=slave, stdout=slave,
stderr=slave, env=env, preexec_fn=controlling_terminal)
os.close(slave)
os.write(master, answers.encode())
@@ -135,6 +146,20 @@ if '--resolve-release' in args:
self.assertIn('Invalid address', output)
self.assertTrue(all(call['fallback'] == '0' for call in calls))
def test_local_bundle_runs_full_interactive_flow_without_fetching_engine(self):
code, output, calls = self.run_wizard('1\n2\n1\n1\n', installed=True, local_bundle=True)
self.assertEqual(code, 0, output)
self.assertIn('Package source: Local staged archive', output)
self.assertFalse(any('url' in call for call in calls))
self.assertIn('--archive', calls[-1]['engine_args'])
self.assertIn('--checksums', calls[-1]['engine_args'])
self.assertIn('update', calls[-1]['engine_args'])
def test_local_bundle_first_install_selects_offline_archive(self):
code, output, calls = self.run_wizard('1\n1\n1\n1\n', local_bundle=True)
self.assertEqual(code, 0, output)
self.assertIn('-offline-linux-arm64.tar.gz', calls[-1]['engine_args'][calls[-1]['engine_args'].index('--archive') + 1])
if __name__ == '__main__':
unittest.main()