diff --git a/Source/Heavy/Toolchain.h b/Source/Heavy/Toolchain.h index 22d8b55951..9581502d18 100644 --- a/Source/Heavy/Toolchain.h +++ b/Source/Heavy/Toolchain.h @@ -8,6 +8,8 @@ #pragma clang diagnostic push #include +#include +#include #include "Constants.h" struct Toolchain { @@ -148,12 +150,14 @@ class ToolchainInstaller final : public Component String downloadLocation = "https://github.com/plugdata-team/plugdata-heavy-toolchain/releases/download/v" + latestVersion + "/"; #if JUCE_MAC - downloadLocation += "Heavy-MacOS-Universal.zip"; + downloadLocation += "Heavy-MacOS-Universal.tar.xz"; #elif JUCE_WINDOWS - downloadLocation += "Heavy-Win64.zip"; + downloadLocation += "Heavy-Win64.tar.xz"; #elif JUCE_LINUX && !__aarch64__ - downloadLocation += "Heavy-Linux-x64.zip"; + downloadLocation += "Heavy-Linux-x64.tar.xz"; #endif + // for testing, you can set a local archive path here + //downloadLocation = "file://Users/timothy/Downloads/Heavy-MacOS-Universal.tar.xz"; instream = URL(downloadLocation).createInputStream(URL::InputStreamOptions(URL::ParameterHandling::inAddress).withConnectionTimeoutMs(10000).withStatusCode(&statusCode)); startThread(); @@ -259,17 +263,160 @@ class ToolchainInstaller final : public Component startTimer(25); - MemoryInputStream input(toolchainData, false); - ZipFile zip(input); - auto const toolchainDir = ProjectInfo::appDataDir.getChildFile("Toolchain"); if (toolchainDir.exists()) toolchainDir.deleteRecursively(); - auto const result = zip.uncompressTo(toolchainDir); + HeapArray decompressedToolchain; +#if JUCE_LINUX || JUCE_WINDOWS + decompressedToolchain.reserve(800 * 1024 * 1024); +#else + decompressedToolchain.reserve(500 * 1024 * 1024); +#endif + bool failed = false; + { + lzma_stream strm = LZMA_STREAM_INIT; + if (lzma_stream_decoder(&strm, UINT64_MAX, 0) != LZMA_OK) { + failed = true; + } + else { + strm.next_in = reinterpret_cast(toolchainData.getData()); + strm.avail_in = toolchainData.getSize(); + + uint8_t buffer[8192]; + lzma_ret ret; + + do { + strm.next_out = buffer; + strm.avail_out = sizeof(buffer); + + ret = lzma_code(&strm, LZMA_FINISH); + size_t written = sizeof(buffer) - strm.avail_out; + decompressedToolchain.insert(decompressedToolchain.end(), buffer, buffer + written); + + if (ret != LZMA_OK && ret != LZMA_STREAM_END) { + failed = true; + lzma_end(&strm); + } + } while (ret != LZMA_STREAM_END); + + lzma_end(&strm); + } + } - if (!result.wasOk() || statusCode >= 400) { + // Parse and extract .tar + // TODO: we do this a few times inline, move it into a helper function along with the xz decompression + auto extractTar = [](const uint8_t* data, size_t size, const File& destRoot) -> bool { + size_t offset = 0; + std::string longLinkName; // For GNU tar @@LongLink entries + + while (offset + 512 <= size) { + const uint8_t* header = data + offset; + if (header[0] == '\0') break; // End of archive + + // Get file name - handle both name and prefix fields for long paths + std::string name; + + if (!longLinkName.empty()) { + // Use the long name from previous @@LongLink entry + name = longLinkName; + longLinkName.clear(); + } else { + // Extract name from header (100 bytes at offset 0) + char nameField[101] = {0}; + std::memcpy(nameField, header, 100); + name = std::string(nameField); + + // Check if there's a prefix field (155 bytes at offset 345) + char prefixField[156] = {0}; + std::memcpy(prefixField, header + 345, 155); + std::string prefix(prefixField); + + if (!prefix.empty()) { + name = prefix + "/" + name; + } + } + + if (name.empty()) break; + + // Clean up the path + name.erase(name.find_last_not_of(" \t\n\r\f\v\0") + 1); + + #if !JUCE_WINDOWS + mode_t mode = static_cast( + std::strtoul(reinterpret_cast(header + 100), nullptr, 8) + ); + bool executable = (mode & 0100) || (mode & 0010) || (mode & 0001); + #endif + + // Get file size (octal) + size_t fileSize = std::strtoull(reinterpret_cast(header + 124), nullptr, 8); + + // Determine type + char typeFlag = header[156]; + + // Handle GNU tar long link entries + if (typeFlag == 'L') { + // This is a @@LongLink entry - read the long filename + size_t fileOffset = offset + 512; + if (fileOffset + fileSize <= size) { + longLinkName.assign(reinterpret_cast(data + fileOffset), fileSize); + // Remove null terminator if present + if (!longLinkName.empty() && longLinkName.back() == '\0') { + longLinkName.pop_back(); + } + } + // Skip this entry and continue + size_t totalEntrySize = 512 + ((fileSize + 511) & ~511); + offset += totalEntrySize; + continue; + } + + // Handle PaxHeaders (POSIX extended headers) + if (typeFlag == 'x' || name.find("PaxHeaders.") == 0) { + // Skip extended header entries for now + // (Full implementation would parse the extended attributes) + size_t totalEntrySize = 512 + ((fileSize + 511) & ~511); + offset += totalEntrySize; + continue; + } + + File outFile = destRoot.getChildFile(String(name)); + + if (typeFlag == '5') { + outFile.createDirectory(); + #if !JUCE_WINDOWS + outFile.setExecutePermission(executable); + #endif + } else if (typeFlag == '0' || typeFlag == '\0') { + outFile.getParentDirectory().createDirectory(); + std::ofstream out(outFile.getFullPathName().toRawUTF8(), std::ios::binary); + size_t fileOffset = offset + 512; + out.write(reinterpret_cast(data + fileOffset), fileSize); + + if (!out.good()) { + out.close(); + outFile.deleteFile(); // cleanup partial file + return false; + } + out.close(); + #if !JUCE_WINDOWS + outFile.setExecutePermission(executable); + #endif + } + + size_t totalEntrySize = 512 + ((fileSize + 511) & ~511); // pad to next 512 + offset += totalEntrySize; + } + return true; + }; + + if(!extractTar(decompressedToolchain.data(), decompressedToolchain.size(), toolchainDir.getParentDirectory())) { + failed = true; + } + + if (failed || statusCode >= 400) { MessageManager::callAsync([this] { installButton.topText = "Try Again"; errorMessage = "Error: Could not extract downloaded package"; @@ -278,28 +425,8 @@ class ToolchainInstaller final : public Component }); return; } - - // Make sure downloaded files have executable permission on unix -#if JUCE_MAC || JUCE_LINUX || JUCE_BSD - - auto const& tcPath = Toolchain::dir.getFullPathName(); - auto const permissionsScript = String("#!/bin/bash") - + "\nchmod +x " + tcPath + "/bin/Heavy/Heavy" - + "\nchmod +x " + tcPath + "/bin/*" - + "\nchmod +x " + tcPath + "/lib/dpf/utils/generate-ttl.sh" - + "\nchmod +x " + tcPath + "/arm-none-eabi/bin/*" - + "\nchmod +x " + tcPath + "/lib/gcc/arm-none-eabi/*/*" - + "\nchmod +x " + tcPath + "/lib/OwlProgram/Tools/*" -# if JUCE_LINUX - + "\nchmod +x " + tcPath + "/x86_64-anywhere-linux-gnu/bin/*" - + "\nchmod +x " + tcPath + "/x86_64-anywhere-linux-gnu/sysroot/sbin/*" - + "\nchmod +x " + tcPath + "/x86_64-anywhere-linux-gnu/sysroot/usr/bin/*" -# endif - ; - - Toolchain::startShellScript(permissionsScript); - -#elif JUCE_WINDOWS + +#if JUCE_WINDOWS File usbDriverInstaller = Toolchain::dir.getChildFile("etc").getChildFile("usb_driver").getChildFile("install-filter.exe"); File driverSpec = Toolchain::dir.getChildFile("etc").getChildFile("usb_driver").getChildFile("DFU_in_FS_Mode.inf"); @@ -340,7 +467,7 @@ class ToolchainInstaller final : public Component float installProgress = 0.0f; bool needsUpdate = false; - int statusCode; + int statusCode = 0; #if JUCE_WINDOWS String downloadSize = "1.2 GB";