diff --git a/Buildscripts/TactilitySDK/Tools/update-sdk.py b/Buildscripts/TactilitySDK/Tools/update-sdk.py
index e32c1c868..b21830e94 100755
--- a/Buildscripts/TactilitySDK/Tools/update-sdk.py
+++ b/Buildscripts/TactilitySDK/Tools/update-sdk.py
@@ -52,36 +52,59 @@ def read_installed_tools_hash(version_directory):
return None
-def extract(archive_data, target_directory):
- """Extracts next to target_directory and then replaces it, refusing entries outside of it"""
- staging_directory = f"{target_directory}.new"
- shutil.rmtree(staging_directory, ignore_errors=True)
- os.makedirs(staging_directory)
- root = os.path.realpath(staging_directory)
- try:
- with zipfile.ZipFile(io.BytesIO(archive_data)) as archive:
- for member in archive.infolist():
- destination = os.path.realpath(os.path.join(root, member.filename))
- if not destination.startswith(root + os.sep):
- fail(f"invalid entry in the archive: {member.filename}")
- archive.extractall(root)
- except zipfile.BadZipFile:
+class Update:
+ """Stages every component next to its installed folder, so a failed download leaves the installation untouched"""
+
+ def __init__(self):
+ # (target directory, staging directory, metadata path, metadata)
+ self.staged = []
+
+ def stage(self, archive_data, target_directory, metadata_path, metadata):
+ """Extracts into a staging folder, refusing entries outside of it"""
+ staging_directory = f"{target_directory}.new"
shutil.rmtree(staging_directory, ignore_errors=True)
- fail("the downloaded archive is invalid")
- shutil.rmtree(target_directory, ignore_errors=True)
- os.replace(staging_directory, target_directory)
+ os.makedirs(staging_directory)
+ self.staged.append((target_directory, staging_directory, metadata_path, metadata))
+ root = os.path.realpath(staging_directory)
+ try:
+ with zipfile.ZipFile(io.BytesIO(archive_data)) as archive:
+ for member in archive.infolist():
+ destination = os.path.realpath(os.path.join(root, member.filename))
+ if not destination.startswith(root + os.sep):
+ fail(f"invalid entry in the archive: {member.filename}")
+ archive.extractall(root)
+ except zipfile.BadZipFile:
+ fail("the downloaded archive is invalid")
+ def discard(self):
+ for _, staging_directory, _, _ in self.staged:
+ shutil.rmtree(staging_directory, ignore_errors=True)
+ self.staged = []
-def install_platform(platform_directory, archive_data, commit):
- extract(archive_data, os.path.join(platform_directory, "TactilitySDK"))
- with open(os.path.join(platform_directory, "commit"), "w") as file:
- file.write(commit)
-
-
-def install_tools(version_directory, archive_data, sha256):
- extract(archive_data, os.path.join(version_directory, "tools"))
- with open(os.path.join(version_directory, "tools.sha256"), "w") as file:
- file.write(sha256)
+ def commit(self):
+ """Replaces the installed folders, and puts the previous ones back when that fails"""
+ replaced = []
+ try:
+ for target_directory, staging_directory, _, _ in self.staged:
+ backup_directory = f"{target_directory}.old"
+ shutil.rmtree(backup_directory, ignore_errors=True)
+ if os.path.isdir(target_directory):
+ os.replace(target_directory, backup_directory)
+ replaced.append((target_directory, backup_directory))
+ os.replace(staging_directory, target_directory)
+ except OSError as error:
+ for target_directory, backup_directory in reversed(replaced):
+ shutil.rmtree(target_directory, ignore_errors=True)
+ if os.path.isdir(backup_directory):
+ os.replace(backup_directory, target_directory)
+ self.discard()
+ fail(f"failed to install the update: {error}")
+ for _, _, metadata_path, metadata in self.staged:
+ with open(metadata_path, "w") as file:
+ file.write(metadata)
+ for _, backup_directory in replaced:
+ shutil.rmtree(backup_directory, ignore_errors=True)
+ self.staged = []
def main():
@@ -99,22 +122,28 @@ def main():
fail("the downloaded index.json is invalid")
commit = index.get("gitCommit", "")
available = index.get("platforms", {})
- updated = False
- tools = index.get("tools")
- if tools is not None and read_installed_tools_hash(version_directory) != tools.get("sha256"):
- print(f"Updating the tools of SDK {version}")
- archive_data = fetch(f"{CDN_URL}/sdk/{version}/{tools['file']}")
- if hashlib.sha256(archive_data).hexdigest() != tools["sha256"]:
- fail("the downloaded tools don't match their checksum")
- install_tools(version_directory, archive_data, tools["sha256"])
- updated = True
- for platform in installed_platforms(version_directory):
- platform_directory = os.path.join(version_directory, f"platform-{platform}")
- if read_commit(platform_directory) == commit or platform not in available:
- continue
- print(f"Updating SDK {version} for {platform}")
- install_platform(platform_directory, fetch(f"{CDN_URL}/sdk/{version}/{available[platform]}"), commit)
- updated = True
+ update = Update()
+ try:
+ tools = index.get("tools")
+ if tools is not None and read_installed_tools_hash(version_directory) != tools.get("sha256"):
+ print(f"Downloading the tools of SDK {version}", flush=True)
+ archive_data = fetch(f"{CDN_URL}/sdk/{version}/{tools['file']}")
+ if hashlib.sha256(archive_data).hexdigest() != tools["sha256"]:
+ fail("the downloaded tools don't match their checksum")
+ update.stage(archive_data, os.path.join(version_directory, "tools"), os.path.join(version_directory, "tools.sha256"), tools["sha256"])
+ for platform in installed_platforms(version_directory):
+ platform_directory = os.path.join(version_directory, f"platform-{platform}")
+ if read_commit(platform_directory) == commit or platform not in available:
+ continue
+ print(f"Downloading SDK {version} for {platform}", flush=True)
+ archive_data = fetch(f"{CDN_URL}/sdk/{version}/{available[platform]}")
+ update.stage(archive_data, os.path.join(platform_directory, "TactilitySDK"), os.path.join(platform_directory, "commit"), commit)
+ except SystemExit:
+ update.discard()
+ print(f"SDK {version} was not changed", file=sys.stderr)
+ raise
+ updated = len(update.staged) > 0
+ update.commit()
with open(os.path.join(version_directory, "index.json"), "wb") as file:
file.write(index_data)
print(f"SDK {version} was updated" if updated else f"SDK {version} is up to date")
diff --git a/Buildscripts/check-tt-version.py b/Buildscripts/check-tt-version.py
index 233d3385b..b30e4cf05 100755
--- a/Buildscripts/check-tt-version.py
+++ b/Buildscripts/check-tt-version.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
-"""Fails when the files of tt changed since a base revision, but TT_VERSION in tt.py wasn't increased.
+"""Fails when the files of tt or its packaging changed since a base revision, but TT_VERSION in tt.py wasn't increased.
Usage: check-tt-version.py , e.g. origin/main"""
import importlib.util
@@ -10,6 +10,8 @@ import sys
ROOT_PATH = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
VERSION_PATTERN = re.compile(r'^TT_VERSION = "([^"]+)"', re.MULTILINE)
+# Changes to what tt.zip contains, e.g. an added or removed file
+PACKAGING_FILE_NAME = "Buildscripts/release-tt.py"
def load_file_names():
@@ -39,7 +41,7 @@ def main():
sys.exit(2)
base = sys.argv[1]
file_names = load_file_names()
- diff = git("diff", "--name-only", f"{base}...HEAD", "--", *file_names)
+ diff = git("diff", "--name-only", f"{base}...HEAD", "--", *file_names, PACKAGING_FILE_NAME)
if diff.returncode != 0:
print(f"git diff failed: {diff.stderr.strip()}", file=sys.stderr)
sys.exit(2)
diff --git a/Documentation/ideas.md b/Documentation/ideas.md
index a3efcc896..ee5280a25 100644
--- a/Documentation/ideas.md
+++ b/Documentation/ideas.md
@@ -7,7 +7,6 @@
## Higher Priority
-- Remove screenshot app and replace with tt.py + screenshot.py: "tt screenshot [filepath]". if filepath isn't specified, save as screenshot-[yyyy-mm-dd].png
- BlueTooth as radio backend for MeshCore/Meshtastic?
- ESP-NOW as radio backend. Use it for MeshCore/Meshtastic?
- Find out why the first build of an app with a clean build cache folder always fails
diff --git a/Modules/app-hub-module/source/app_hub_command.cpp b/Modules/app-hub-module/source/app_hub_command.cpp
index 6b516a87a..41470eb1b 100644
--- a/Modules/app-hub-module/source/app_hub_command.cpp
+++ b/Modules/app-hub-module/source/app_hub_command.cpp
@@ -40,6 +40,7 @@ static int report_error(const char* action, error_t error) {
/**
* Queues an operation and waits for its result.
* The subscription is made before queueing, so the operation can't finish unnoticed.
+ * It times out after OPERATION_TIMEOUT without events of the operation, so a long update of many apps doesn't.
* @param[in] finished_type the event that ends the operation and carries its result
* @param[in] on_event called for the other events of the operation (optional)
*/
@@ -62,7 +63,7 @@ static error_t run_and_wait(
error = operation(id, &operation_id);
if (error == ERROR_NONE) {
error = ERROR_TIMEOUT;
- const TickType_t start = get_ticks();
+ TickType_t start = get_ticks();
TickType_t elapsed = 0;
bool finished = false;
while (!finished && elapsed < OPERATION_TIMEOUT) {
@@ -72,6 +73,7 @@ static error_t run_and_wait(
if (event.operation_id != operation_id) {
continue;
}
+ start = get_ticks();
if (event.type == finished_type) {
error = event.result;
finished = true;
diff --git a/Modules/http-module/private/http/private/http_parsing.h b/Modules/http-module/private/http/private/http_parsing.h
index 5e279561f..c0165ce21 100644
--- a/Modules/http-module/private/http/private/http_parsing.h
+++ b/Modules/http-module/private/http/private/http_parsing.h
@@ -20,5 +20,8 @@ bool http_parse_status_line(const std::string& line, int& statusCode);
/** Parses a header line such as "Content-Length: 42". The name is returned in lower case, the value without surrounding whitespace. */
bool http_parse_header_line(const std::string& line, std::string& name, std::string& value);
+/** Parses a Content-Length value: decimal digits only, without sign or overflow */
+bool http_parse_content_length(const std::string& value, long& length);
+
/** Parses the size line of a chunk in a chunked body, e.g. "1a2b" or "1a2b;name=value" */
bool http_parse_chunk_size(const std::string& line, size_t& size);
diff --git a/Modules/http-module/source/download_posix.cpp b/Modules/http-module/source/download_posix.cpp
index aa4db8603..6f5654ff1 100644
--- a/Modules/http-module/source/download_posix.cpp
+++ b/Modules/http-module/source/download_posix.cpp
@@ -299,6 +299,8 @@ HttpDownloadEvent http_download_run(const std::string& url, const std::string& c
// -1 means the length is unknown: the body is chunked or ends when the server closes the connection
long contentLength = -1;
+ std::string contentLengthValue;
+ bool hasContentLength = false;
bool chunked = false;
while (true) {
if (!connection.readLine(line)) {
@@ -313,7 +315,8 @@ HttpDownloadEvent http_download_run(const std::string& url, const std::string& c
continue;
}
if (name == "content-length") {
- contentLength = strtol(value.c_str(), nullptr, 10);
+ contentLengthValue = value;
+ hasContentLength = true;
} else if (name == "transfer-encoding" && value.find("chunked") != std::string::npos) {
chunked = true;
}
@@ -322,6 +325,10 @@ HttpDownloadEvent http_download_run(const std::string& url, const std::string& c
if (statusCode < 200 || statusCode >= 300) {
return http_download_make_error_event("Server response is not OK", statusCode);
}
+ // A chunked body ignores the Content-Length
+ if (hasContentLength && !chunked && !http_parse_content_length(contentLengthValue, contentLength)) {
+ return http_download_make_error_event("Invalid Content-Length", statusCode);
+ }
if (http_download_is_cancelled(link)) {
return http_download_make_cancelled_event(statusCode);
}
diff --git a/Modules/http-module/source/http_parsing.cpp b/Modules/http-module/source/http_parsing.cpp
index 61d4730b6..f783b4bb1 100644
--- a/Modules/http-module/source/http_parsing.cpp
+++ b/Modules/http-module/source/http_parsing.cpp
@@ -2,6 +2,7 @@
#include
#include
+#include
#include
#include
@@ -110,6 +111,22 @@ bool http_parse_header_line(const std::string& line, std::string& name, std::str
return !name.empty();
}
+bool http_parse_content_length(const std::string& value, long& length) {
+ if (value.empty() || value.find_first_not_of("0123456789") != std::string::npos) {
+ return false;
+ }
+ long result = 0;
+ for (const char c : value) {
+ const long digit = c - '0';
+ if (result > (LONG_MAX - digit) / 10) {
+ return false;
+ }
+ result = result * 10 + digit;
+ }
+ length = result;
+ return true;
+}
+
bool http_parse_chunk_size(const std::string& line, size_t& size) {
const std::string digits = trim(line.substr(0, line.find(';')));
if (digits.empty() || digits.size() > sizeof(size_t) * 2) {
diff --git a/Modules/http-module/tests/source/http_parsing_test.cpp b/Modules/http-module/tests/source/http_parsing_test.cpp
index de9ac5227..b04b282a6 100644
--- a/Modules/http-module/tests/source/http_parsing_test.cpp
+++ b/Modules/http-module/tests/source/http_parsing_test.cpp
@@ -46,6 +46,21 @@ TEST_CASE("http_parse_header_line lowercases the name and trims the value") {
CHECK_FALSE(http_parse_header_line(": empty name", name, value));
}
+TEST_CASE("http_parse_content_length accepts decimal digits only") {
+ long length = -1;
+ REQUIRE(http_parse_content_length("42", length));
+ CHECK_EQ(length, 42);
+ REQUIRE(http_parse_content_length("0", length));
+ CHECK_EQ(length, 0);
+ CHECK_FALSE(http_parse_content_length("", length));
+ CHECK_FALSE(http_parse_content_length("-5", length));
+ CHECK_FALSE(http_parse_content_length("+5", length));
+ CHECK_FALSE(http_parse_content_length("12abc", length));
+ CHECK_FALSE(http_parse_content_length("1 2", length));
+ CHECK_FALSE(http_parse_content_length("99999999999999999999999", length));
+ CHECK_EQ(length, 0);
+}
+
TEST_CASE("http_parse_chunk_size reads hexadecimal sizes and ignores extensions") {
size_t size = 0;
REQUIRE(http_parse_chunk_size("1a2B", size));
diff --git a/Tactility/Private/Tactility/app/timezone/TimeZoneView.h b/Tactility/Private/Tactility/app/timezone/TimeZoneView.h
index 6ab14d764..ff664a134 100644
--- a/Tactility/Private/Tactility/app/timezone/TimeZoneView.h
+++ b/Tactility/Private/Tactility/app/timezone/TimeZoneView.h
@@ -27,7 +27,7 @@ public:
explicit TimeZoneView(OnSelected onSelected);
- /** Waits for a running detection, which can take a few seconds. Destroy it without holding the LVGL lock. */
+ /** Cancels a running detection and waits for its lookup to end, which can take a few seconds */
~TimeZoneView();
TimeZoneView(const TimeZoneView&) = delete;
@@ -59,6 +59,8 @@ private:
std::unique_ptr filterTimer;
std::unique_ptr detectThread;
std::atomic detecting = false;
+ // Set when the view is destroyed, so a running detection ends without using it
+ std::atomic cancelled = false;
// Valid while the widgets exist, guarded by the LVGL lock
lv_obj_t* textarea = nullptr;
diff --git a/Tactility/Source/app/TileGrid.cpp b/Tactility/Source/app/TileGrid.cpp
index 01c0496db..8ed110ddd 100644
--- a/Tactility/Source/app/TileGrid.cpp
+++ b/Tactility/Source/app/TileGrid.cpp
@@ -90,6 +90,40 @@ void updatePageIndicatorWidth(void* data) {
}
}
+// A page indicator below the grid gets the width of its parent.
+// It shows the page number when its dots are wider than its max width, which must be in pixels.
+void updateCenteredPageIndicatorWidth(lv_obj_t* indicator) {
+ const int32_t margins = lv_obj_get_style_margin_left(indicator, LV_PART_MAIN) + lv_obj_get_style_margin_right(indicator, LV_PART_MAIN);
+ const int32_t max_width = std::max(0, lv_obj_get_content_width(lv_obj_get_parent(indicator)) - margins);
+ // Setting a style marks the layout as dirty, which would resize the parent and call this again
+ if (lv_obj_get_style_max_width(indicator, LV_PART_MAIN) != max_width) {
+ lv_obj_set_style_max_width(indicator, max_width, LV_STATE_DEFAULT);
+ }
+}
+
+void onCenteredPageIndicatorParentSizeChanged(lv_event_t* event) {
+ updateCenteredPageIndicatorWidth(static_cast(lv_event_get_user_data(event)));
+}
+
+void onCenteredPageIndicatorDeleted(lv_event_t* event) {
+ lv_obj_t* indicator = lv_event_get_target_obj(event);
+ lv_obj_remove_event_cb_with_user_data(lv_obj_get_parent(indicator), onCenteredPageIndicatorParentSizeChanged, indicator);
+}
+
+/** Call after moving the page indicator below the grid */
+void centerPageIndicator(lv_obj_t* indicator) {
+ lv_obj_set_width(indicator, LV_PCT(100));
+ lv_obj_add_event_cb(lv_obj_get_parent(indicator), onCenteredPageIndicatorParentSizeChanged, LV_EVENT_SIZE_CHANGED, indicator);
+ lv_obj_add_event_cb(indicator, onCenteredPageIndicatorDeleted, LV_EVENT_DELETE, nullptr);
+ updateCenteredPageIndicatorWidth(indicator);
+}
+
+/** Call before moving the page indicator away from below the grid */
+void uncenterPageIndicator(lv_obj_t* indicator) {
+ lv_obj_remove_event_cb_with_user_data(lv_obj_get_parent(indicator), onCenteredPageIndicatorParentSizeChanged, indicator);
+ lv_obj_remove_event_cb(indicator, onCenteredPageIndicatorDeleted);
+}
+
void onBottomBarSizeChanged(lv_event_t* event) {
lv_async_call_cancel(updatePageIndicatorWidth, lv_event_get_target_obj(event));
lv_async_call(updatePageIndicatorWidth, lv_event_get_target_obj(event));
@@ -448,7 +482,7 @@ void TileGrid::createWidgets(lv_obj_t* parent) {
createGrid(parent);
// Centered below the grid
pageIndicator = lvgl_page_indicator_create(parent);
- lv_obj_set_width(pageIndicator, LV_PCT(100));
+ centerPageIndicator(pageIndicator);
// Keeps the dots off the screen's bottom edge
lv_obj_set_style_margin_bottom(pageIndicator, lvgl_theme_is_compact() ? LV_DPX(2) : LV_DPX(8), LV_STATE_DEFAULT);
finishCreate(parent);
@@ -531,8 +565,7 @@ void TileGrid::setBarSideContainer(lv_obj_t* sideContainer) {
// The page indicator is centered below the grid
lv_obj_set_parent(pageIndicator, barParent);
lv_obj_move_to_index(pageIndicator, lv_obj_get_index(grid) + 1);
- lv_obj_set_width(pageIndicator, LV_PCT(100));
- lv_obj_remove_local_style_prop(pageIndicator, LV_STYLE_MAX_WIDTH, LV_PART_MAIN);
+ centerPageIndicator(pageIndicator);
// Keeps the dots off the screen's bottom edge
lv_obj_set_style_margin_bottom(pageIndicator, lvgl_theme_is_compact() ? 1 : 2, LV_STATE_DEFAULT);
} else {
@@ -543,9 +576,13 @@ void TileGrid::setBarSideContainer(lv_obj_t* sideContainer) {
lv_obj_remove_local_style_prop(bottomBar, LV_STYLE_BG_OPA, LV_PART_MAIN);
lv_obj_remove_local_style_prop(bottomBar, LV_STYLE_PAD_ROW, LV_PART_MAIN);
lv_obj_set_hidden(barSpacer, false);
+ uncenterPageIndicator(pageIndicator);
lv_obj_set_parent(pageIndicator, bottomBar);
lv_obj_set_width(pageIndicator, LV_SIZE_CONTENT);
lv_obj_remove_local_style_prop(pageIndicator, LV_STYLE_MARGIN_BOTTOM, LV_PART_MAIN);
+ // The bar's size can stay the same, which wouldn't update the max width
+ lv_async_call_cancel(updatePageIndicatorWidth, bottomBar);
+ lv_async_call(updatePageIndicatorWidth, bottomBar);
}
// The keys move through the bar where it's shown: below the statusbar's icons in the side strip, or below the grid
setGroupOrder(grid, bottomBar, sideContainer != nullptr);
diff --git a/Tactility/Source/app/timezone/TimeZoneView.cpp b/Tactility/Source/app/timezone/TimeZoneView.cpp
index dcd758e67..2ddc341a2 100644
--- a/Tactility/Source/app/timezone/TimeZoneView.cpp
+++ b/Tactility/Source/app/timezone/TimeZoneView.cpp
@@ -78,6 +78,7 @@ TimeZoneView::TimeZoneView(OnSelected onSelected) : onSelected(std::move(onSelec
TimeZoneView::~TimeZoneView() {
filterTimer->stop();
+ cancelled = true;
if (detectThread != nullptr) {
detectThread->join();
}
@@ -268,7 +269,16 @@ void TimeZoneView::detect() {
failure = "The time zone couldn't be detected. Check the internet connection and try again.";
}
- lvgl_lock();
+ // Waits for the LVGL lock without blocking the destructor, which can be called with the lock held
+ while (!lvgl_try_lock(pdMS_TO_TICKS(50))) {
+ if (cancelled) {
+ return;
+ }
+ }
+ if (cancelled) {
+ lvgl_unlock();
+ return;
+ }
detecting = false;
mutex.lock();
message = failure;
diff --git a/tt.py b/tt.py
index f05ce0820..b15e27d45 100755
--- a/tt.py
+++ b/tt.py
@@ -20,7 +20,7 @@ import time
import urllib.request
import zipfile
-TT_VERSION = "1.1.0"
+TT_VERSION = "1.1.1"
CDN_URL = os.environ.get("TT_CDN", "https://cdn.tactilityproject.org")
HOME_DIRECTORY = os.path.join(os.path.expanduser("~"), ".tactility")
INDEX_VALIDITY_SECONDS = 3600
@@ -173,15 +173,47 @@ def install_platforms(version, version_directory, index, platforms):
file.write(index.get("gitCommit", ""))
+def read_text(path):
+ try:
+ with open(path) as file:
+ return file.read().strip()
+ except OSError:
+ return None
+
+
+def is_installed_build(version_directory, index):
+ """True when the installed tools and platforms are of the build that the index describes"""
+ installed_tools_hash = read_text(os.path.join(version_directory, "tools.sha256"))
+ if installed_tools_hash is not None and installed_tools_hash != index.get("tools", {}).get("sha256"):
+ return False
+ for name in os.listdir(version_directory):
+ commit = read_text(os.path.join(version_directory, name, "commit"))
+ if name.startswith("platform-") and commit is not None and commit != index.get("gitCommit", ""):
+ return False
+ return True
+
+
def prepare_sdk(version, platforms):
"""Installs what's missing of the SDK version. Installed tools and platforms are only replaced by "tt update-sdk", so they stay from the same build."""
version_directory = version_path(HOME_DIRECTORY, version)
+ index = None
+ # "target.platforms=all" means every platform of the SDK
+ if platforms == ["all"]:
+ os.makedirs(version_directory, exist_ok=True)
+ index = load_index(version, version_directory)
+ platforms = sorted(index.get("platforms", {}))
has_tools = os.path.isdir(os.path.join(version_directory, "tools"))
missing_platforms = [platform for platform in platforms if not os.path.isdir(os.path.join(version_directory, f"platform-{platform}", "TactilitySDK"))]
if has_tools and not missing_platforms:
return version_directory
os.makedirs(version_directory, exist_ok=True)
- index = load_index(version, version_directory)
+ if index is None:
+ index = load_index(version, version_directory)
+ # The CDN only has the latest build of a version, so a newer build can't be mixed with the installed one
+ if not is_installed_build(version_directory, index):
+ if has_tools:
+ fail(f"SDK {version} was published again since it was installed: run 'tt update-sdk' first")
+ fail(f"SDK {version} was published again since it was installed: remove {version_directory} and try again")
if not has_tools:
install_tools(version, version_directory, index)
install_platforms(version, version_directory, index, missing_platforms)
@@ -255,7 +287,9 @@ def main(arguments):
fail(f"tools not found: {tools_directory}")
else:
sdk_path = HOME_DIRECTORY
- tools_directory = os.path.join(prepare_sdk(version, platforms), "tools")
+ # update-sdk replaces the installed build, so it only needs the tools
+ required_platforms = [] if command == "update-sdk" else platforms
+ tools_directory = os.path.join(prepare_sdk(version, required_platforms), "tools")
environment = tool_environment(version, sdk_path, is_local)
if is_help:
print_help(version, tools_directory)