diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..5f8aeb3 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,255 @@ +name: Build clients + +on: + push: + branches: [main] + tags: ["v*"] + pull_request: + workflow_dispatch: + +env: + QT_VERSION: "6.11.1" + +jobs: + macos: + name: macOS (universal x86_64 + arm64) + runs-on: macos-14 + steps: + - uses: actions/checkout@v4 + + - name: Install Qt + uses: jurplel/install-qt-action@v4 + with: + version: ${{ env.QT_VERSION }} + target: desktop + cache: true + + - name: Build + run: | + qmake -recursive QMAKE_APPLE_DEVICE_ARCHS="x86_64 arm64" MACOSX_DEPLOYMENT_TARGET="12.7.6" + make -j"$(sysctl -n hw.ncpu)" + + - name: Bundle app icon + run: | + mkdir -p ApolloExplorerPC/ApolloExplorer.app/Contents/Resources/ + cp ApolloExplorerPC/icons/ApolloExplorer.icns ApolloExplorerPC/ApolloExplorer.app/Contents/Resources/ + + - name: Deploy Qt frameworks + run: macdeployqt ApolloExplorerPC/ApolloExplorer.app -verbose=1 + + - name: Ad-hoc code sign + # Unsigned/unnotarized: no Apple Developer ID is available in CI. Ad-hoc + # signing (identity "-") is required regardless, since arm64 refuses to + # run any unsigned Mach-O binary at all. Users must still right-click -> + # Open (or `xattr -cr`) the first time because of the quarantine flag. + # + # TODO: real signing/notarization, deferred for now. build_macos_client.sh + # does this locally with an Apple Developer Team ID (${APPLETEAMID}) via + # `codesign --sign ${APPLETEAMID}` + `macdeployqt -sign-for-notarization=` + # + `xcrun notarytool`. To wire this into CI: add the Developer ID cert + # (base64 .p12) and APPLETEAMID as repo secrets, import the cert into a + # temporary keychain, sign with it instead of "-", then notarize + staple + # before packaging. + run: | + codesign --force --deep --sign - ApolloExplorerPC/ApolloExplorer.app + codesign --force --sign - acp/acp + codesign --force --sign - ash/ash + + - name: Package + run: | + mkdir -p ApolloExplorer-macOS + mv ApolloExplorerPC/ApolloExplorer.app ApolloExplorer-macOS/ + mv acp/acp ApolloExplorer-macOS/ + mv ash/ash ApolloExplorer-macOS/ + ditto -c -k --sequesterRsrc --keepParent ApolloExplorer-macOS ApolloExplorer-macOS-universal.zip + + # archive: false => the download is this zip itself, not a zip wrapping it. + - uses: actions/upload-artifact@v7 + with: + path: ApolloExplorer-macOS-universal.zip + archive: false + + linux: + name: Linux (x86_64) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Qt + uses: jurplel/install-qt-action@v4 + with: + version: ${{ env.QT_VERSION }} + target: desktop + install-deps: true + cache: true + + - name: Install build tools + run: sudo apt-get update && sudo apt-get install -y build-essential + + - name: Build + run: | + qmake -recursive + make -j"$(nproc)" + + - name: Package + run: | + mkdir -p ApolloExplorer-Linux + mv ApolloExplorerPC/ApolloExplorer ApolloExplorer-Linux/ApolloExplorer + cp ApolloExplorerPC/ApolloExplorer.desktop ApolloExplorer-Linux/ApolloExplorer.desktop + mv acp/acp ApolloExplorer-Linux/acp + mv ash/ash ApolloExplorer-Linux/ash + tar -czf ApolloExplorer-Linux-x86_64.tar.gz ApolloExplorer-Linux + + # archive: false => the download is the tar.gz itself (keeps exec bits). + - uses: actions/upload-artifact@v7 + with: + path: ApolloExplorer-Linux-x86_64.tar.gz + archive: false + + windows: + name: Windows (x86_64) + runs-on: windows-latest + env: + # aqtinstall cannot resolve any 6.11.x version for Windows targets: Qt + # restructured its Windows repo layout starting at 6.11.0 (added an + # arm64 cross-compile variant folder), which breaks Updates.xml + # resolution upstream in aqtinstall. Pin to the last known-good release + # here instead of env.QT_VERSION until aqtinstall ships a fix. + QT_VERSION: "6.10.1" + steps: + - uses: actions/checkout@v4 + + - name: Install Qt + uses: jurplel/install-qt-action@v4 + with: + version: ${{ env.QT_VERSION }} + target: desktop + arch: win64_msvc2022_64 + cache: true + + - name: Set up MSVC environment + uses: ilammy/msvc-dev-cmd@v1 + with: + arch: amd64 + + - name: Build + shell: cmd + run: | + qmake -recursive -config release + nmake + + - name: Package + shell: pwsh + run: | + # ash is excluded on win32 in ApolloExplorer.pro (see comment there): + # it needs POSIX termios, which Windows doesn't have. + New-Item -ItemType Directory -Force -Path ApolloExplorer-Windows + Move-Item ApolloExplorerPC\release\ApolloExplorer.exe ApolloExplorer-Windows\ApolloExplorer.exe + Move-Item acp\release\acp.exe ApolloExplorer-Windows\acp.exe + # Bundle the Qt DLLs, plugins and MSVC runtime next to the exes; the + # .pro files' post-link windeployqt only fills release\, not this folder. + windeployqt --release --compiler-runtime ApolloExplorer-Windows\ApolloExplorer.exe + windeployqt --release --compiler-runtime ApolloExplorer-Windows\acp.exe + # Zip the folder itself so it unzips to ApolloExplorer-Windows\. + Compress-Archive -Path ApolloExplorer-Windows -DestinationPath ApolloExplorer-Windows-x86_64.zip + + # archive: false => the download is this zip itself, not a zip wrapping it. + - uses: actions/upload-artifact@v7 + with: + path: ApolloExplorer-Windows-x86_64.zip + archive: false + + amiga: + name: Amiga server (m68k, experimental) + runs-on: ubuntu-latest + # Best-effort: this cross-toolchain isn't officially supported by upstream + # and hasn't been verified against real hardware/emulator yet. A failure + # here must not block the desktop-client release. + continue-on-error: true + steps: + - uses: actions/checkout@v4 + + - name: Install toolchain build dependencies + run: | + sudo apt-get update + sudo apt-get install -y build-essential lhasa gettext texinfo flex bison \ + libgmp-dev libmpfr-dev libmpc-dev rsync + + - name: Cache amiga-gcc toolchain + id: cache-toolchain + uses: actions/cache@v4 + with: + path: /opt/amiga + key: amiga-gcc-${{ runner.os }}-amiga6-ndk3.2-libamiga-v1 + + - name: Build m68k-amigaos-gcc toolchain (bebbo/amiga-gcc) + if: steps.cache-toolchain.outputs.cache-hit != 'true' + run: | + # bebbo/amiga-gcc migrated off GitHub; Codeberg is its current home. + git clone --depth 1 https://codeberg.org/bebbo/amiga-gcc /tmp/amiga-gcc + cd /tmp/amiga-gcc + make update + # `min` (binutils gcc gprof libnix libgcc libnix4.library) fails + # deterministically (not a -j race - reproduces serially too): + # upstream's libnix4-support generator emits a duplicate + # __vfwprintf_total_size stub, breaking export-libnix4.c. That + # target builds libnix4.library, a shared-library variant of + # libnix that Amiga/Makefile never uses (it links -noixemul, + # i.e. plain static libnix) - so build min's other pieces + # directly and skip the broken, unneeded target. + # ndk/netinclude/libamiga: NDK3.2 headers + -lamiga, as before. + make binutils gcc gprof libnix libgcc ndk netinclude libamiga NDK=3.2 PREFIX=/opt/amiga -j"$(nproc)" + + - name: Bridge NDK includes to the ApolloCrossDev layout + # Amiga/Makefile (the WDrijver/ApolloCrossDev toolchain convention) expects + # headers at ndk32-include/Include_H — the raw Aminet NDK3.2 layout — while + # bebbo/amiga-gcc installs its generated, netinclude-merged headers at + # ndk-include/. Symlink the two so both conventions resolve to the same, + # complete header set without duplicating or hand-maintaining headers. + run: | + mkdir -p /opt/amiga/m68k-amigaos/ndk32-include + ln -sfn /opt/amiga/m68k-amigaos/ndk-include /opt/amiga/m68k-amigaos/ndk32-include/Include_H + + - name: Build + run: | + export PATH="/opt/amiga/bin:$PATH" + # Amiga/Makefile's first (default) target is just the server binary; + # `all` is required to also build ApolloExplorerTool. + make -C Amiga PREFIX=/opt/amiga all + + - name: Package + run: | + mkdir -p ApolloExplorer-Amiga + mv Amiga/ApolloExplorerSrv ApolloExplorer-Amiga/ + mv Amiga/ApolloExplorerTool ApolloExplorer-Amiga/ + # Workbench icons + the AEKill IconX script (runs `ApolloExplorerTool kill`), + # same set as the upstream release's "Amiga Server" folder. + cp Amiga/dist/ApolloExplorerSrv.info Amiga/dist/AEKill Amiga/dist/AEKill.info ApolloExplorer-Amiga/ + # Unzips to an ApolloExplorer-Amiga/ folder. + zip -r ApolloExplorer-Amiga-m68k.zip ApolloExplorer-Amiga + + # archive: false => the download is this zip itself, not a zip wrapping it. + - uses: actions/upload-artifact@v7 + with: + path: ApolloExplorer-Amiga-m68k.zip + archive: false + + release: + name: Attach artifacts to release + if: startsWith(github.ref, 'refs/tags/v') + needs: [macos, linux, windows, amiga] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + # v8 is required to download artifacts uploaded with `archive: false`. + - uses: actions/download-artifact@v8 + with: + path: artifacts + merge-multiple: true + + - run: ls -laR artifacts + + - uses: softprops/action-gh-release@v2 + with: + files: artifacts/**/* diff --git a/Amiga/dist/AEKill b/Amiga/dist/AEKill new file mode 100644 index 0000000..4c4f437 --- /dev/null +++ b/Amiga/dist/AEKill @@ -0,0 +1 @@ +ApolloExplorerTool kill diff --git a/Amiga/dist/AEKill.info b/Amiga/dist/AEKill.info new file mode 100644 index 0000000..673a657 Binary files /dev/null and b/Amiga/dist/AEKill.info differ diff --git a/Amiga/dist/ApolloExplorerSrv.info b/Amiga/dist/ApolloExplorerSrv.info new file mode 100644 index 0000000..82870ff Binary files /dev/null and b/Amiga/dist/ApolloExplorerSrv.info differ diff --git a/AmigaIconReader/AmigaInfoFile.cpp b/AmigaIconReader/AmigaInfoFile.cpp index a0e48a0..b6f4ff1 100644 --- a/AmigaIconReader/AmigaInfoFile.cpp +++ b/AmigaIconReader/AmigaInfoFile.cpp @@ -106,8 +106,7 @@ static inline void putByteArrayInBuffer( QByteArray &someBuffer, quint64 &someOf static inline QByteArray BitPlanarToChunky( QByteArray bitPlanarPixels, quint32 width, quint32 height, quint8 bitsPerPixel ) { //Set aside some memory for our chunky icon - char indexedImageData[ width * height ]; - memset( indexedImageData, 0, width * height ); + QByteArray indexedImageData( static_cast( width * height ), 0 ); //Load in the bitplanar bitmap quint64 pitplaneRowByteLength = ((width+15)/16)*2; @@ -123,15 +122,15 @@ static inline QByteArray BitPlanarToChunky( QByteArray bitPlanarPixels, quint32 quint8 bit = 7-x%8; quint8 srcPixelByte = bitPlanarPixels.constData()[ (bitPlane * bitplaneSizeInBytes ) + ( y*pitplaneRowByteLength ) + ( x/8 ) ]; srcPixelByte = qFromBigEndian( srcPixelByte ); - quint8 dstPixelByte = indexedImageData[ x+(y*width) ]; + quint8 dstPixelByte = static_cast( indexedImageData[ x+(y*width) ] ); quint8 bitComparator = (1<( resultingPixelByte ); } } } - return QByteArray( indexedImageData, width * height ); + return indexedImageData; } @@ -560,8 +559,7 @@ qint16 AmigaInfoFile::getPriority() QByteArray AmigaInfoFile::decodeOS2Icon(QByteArray data, quint64 &offset, quint8 depth, quint32 width, quint32 height ) { //Set aside some memory for our chunky icon - char indexedImageData[ width * height ]; - memset( indexedImageData, 0, width * height ); + QByteArray indexedImageData( static_cast( width * height ), 0 ); //Load in the bitplanar bitmap quint64 pitplaneRowByteLength = ((width+15)/16)*2; @@ -578,10 +576,10 @@ QByteArray AmigaInfoFile::decodeOS2Icon(QByteArray data, quint64 &offset, quint8 quint8 bit = 7-x%8; quint8 srcPixelByte = bitPlanes.constData()[ (bitPlane * bitplaneSizeInBytes ) + ( y*pitplaneRowByteLength ) + ( x/8 ) ]; srcPixelByte = qFromBigEndian( srcPixelByte ); - quint8 dstPixelByte = indexedImageData[ x+(y*width) ]; + quint8 dstPixelByte = static_cast( indexedImageData[ x+(y*width) ] ); quint8 bitComparator = (1<( resultingPixelByte ); } } } @@ -589,7 +587,7 @@ QByteArray AmigaInfoFile::decodeOS2Icon(QByteArray data, quint64 &offset, quint8 //Adjust the offset offset += bitplaneSizeInBytes * depth; - return QByteArray( indexedImageData, width * height ); + return indexedImageData; } diff --git a/AmigaIconReader/AmigaInfoFile.h b/AmigaIconReader/AmigaInfoFile.h index aba27f9..938efa4 100644 --- a/AmigaIconReader/AmigaInfoFile.h +++ b/AmigaIconReader/AmigaInfoFile.h @@ -20,7 +20,20 @@ typedef quint16 UWORD; typedef qint8 BYTE; #endif +// __attribute__((packed)) is a GCC/Clang extension; MSVC's cl.exe doesn't +// understand it and fails to parse the struct at all. Use #pragma pack on +// MSVC instead to get the same tightly-packed, wire-format-compatible layout. +#if defined(_MSC_VER) +#define AMIGA_PACKED_STRUCT_BEGIN __pragma(pack(push, 1)) +#define AMIGA_PACKED_STRUCT_END __pragma(pack(pop)) +#define AMIGA_PACKED +#else +#define AMIGA_PACKED_STRUCT_BEGIN +#define AMIGA_PACKED_STRUCT_END +#define AMIGA_PACKED __attribute__((packed)) +#endif +AMIGA_PACKED_STRUCT_BEGIN typedef struct { qint16 LeftEdge; /* starting offset relative to some origin */ @@ -32,8 +45,10 @@ typedef struct quint8 PlanePick; quint8 PlaneOnOff; quint32 NextImage; -} __attribute__((packed)) Image_t; +} AMIGA_PACKED Image_t; +AMIGA_PACKED_STRUCT_END +AMIGA_PACKED_STRUCT_BEGIN typedef struct { quint32 ga_Next; //always 0 qint16 ga_LeftEdge; @@ -59,8 +74,10 @@ typedef struct { quint32 ga_SpecialInfo; // quint16 ga_GadgetID; // quint32 ga_UserData; -} __attribute__((packed)) Gadget_t; +} AMIGA_PACKED Gadget_t; +AMIGA_PACKED_STRUCT_END +AMIGA_PACKED_STRUCT_BEGIN struct NewWindow { #if 0 @@ -81,21 +98,26 @@ struct NewWindow char padding[48]; #endif -} __attribute__((packed)); +} AMIGA_PACKED; +AMIGA_PACKED_STRUCT_END +AMIGA_PACKED_STRUCT_BEGIN typedef struct { quint32 dd_Flags; /* flags for drawer */ quint16 dd_ViewModes; /* view mode for drawer */ -} __attribute__((packed)) DrawerData2_t; +} AMIGA_PACKED DrawerData2_t; +AMIGA_PACKED_STRUCT_END +AMIGA_PACKED_STRUCT_BEGIN typedef struct { struct NewWindow dd_NewWindow; /* args to open window */ qint32 dd_CurrentX; /* current x coordinate of origin */ qint32 dd_CurrentY; /* current y coordinate of origin */ //DrawerData2_t dd_Extended; -} __attribute__((packed)) DrawerData_t; +} AMIGA_PACKED DrawerData_t; +AMIGA_PACKED_STRUCT_END @@ -109,8 +131,9 @@ typedef enum DEVICE = 6, KICK = 7, APPICON = 8 -} __attribute__((packed)) IconType_t; +} IconType_t; +AMIGA_PACKED_STRUCT_BEGIN typedef struct { quint16 do_Magic; //magic number at start of file. Always 0xE310 quint16 do_Version; //Always 1 @@ -133,7 +156,8 @@ typedef struct { //ToolWindow text if ic_ToolWindow not zero (format see below) // this is an extension, which was never implemented //struct DrawerData2 -} __attribute__((packed)) OS2Icon_t; +} AMIGA_PACKED OS2Icon_t; +AMIGA_PACKED_STRUCT_END class AmigaInfoFile : public QObject { diff --git a/ApolloExplorer.pro b/ApolloExplorer.pro index 6ee0c03..cb60729 100755 --- a/ApolloExplorer.pro +++ b/ApolloExplorer.pro @@ -1,4 +1,11 @@ TEMPLATE=subdirs SUBDIRS=ApolloExplorerPC AmigaIconReader \ - acp \ - ash + acp + +# ash is an interactive terminal client built on POSIX termios/tcsetattr and +# raw STDIN_FILENO reads, none of which exist on Windows. It was never built +# by build_windows_client.ps1 either; porting it to the Windows Console API +# is a separate, larger effort. +!win32 { + SUBDIRS += ash +} diff --git a/ApolloExplorerPC/AEUtils.h b/ApolloExplorerPC/AEUtils.h index b816a68..8eefb01 100755 --- a/ApolloExplorerPC/AEUtils.h +++ b/ApolloExplorerPC/AEUtils.h @@ -5,12 +5,18 @@ #include #include +#if defined(_MSC_VER) +#define AE_PRETTY_FUNCTION __FUNCSIG__ +#else +#define AE_PRETTY_FUNCTION __PRETTY_FUNCTION__ +#endif + #if DEBUG > 0 -#define DBGLOG qDebug().noquote() << __PRETTY_FUNCTION__ +#define DBGLOG qDebug().noquote() << AE_PRETTY_FUNCTION #else #define DBGLOG QNoDebug() #endif -#define WARNLOG qWarning().noquote() << __PRETTY_FUNCTION__ +#define WARNLOG qWarning().noquote() << AE_PRETTY_FUNCTION QString convertFromAmigaTextEncoding( char *text ); void convertFromUTF8ToAmigaTextEncoding( QString utf8Text, char *encodedText, quint64 encodedTextLength ); diff --git a/ApolloExplorerPC/scanningwindow.cpp b/ApolloExplorerPC/scanningwindow.cpp index 5f1b209..b9cbe8a 100755 --- a/ApolloExplorerPC/scanningwindow.cpp +++ b/ApolloExplorerPC/scanningwindow.cpp @@ -91,7 +91,10 @@ ScanningWindow::ScanningWindow(QWidget *parent) : } m_Settings->endGroup(); - //Load the static hosts + //Load the static hosts. + //Fallbacks must be off: on macOS they pull in the global preferences domain, + //whose "com.apple.*" keys show up as bogus groups (e.g. "com") => an "INVALID" host. + m_StaticHosts->setFallbacksEnabled( false ); populateStaticHosts(); } @@ -139,6 +142,11 @@ void ScanningWindow::populateStaticHosts() for( auto iter = staticHostList.begin(); iter != staticHostList.end(); iter++ ) { //Extract from the hosts file QString hostIP = (*iter); + QHostAddress ipAddress; + if( !ipAddress.setAddress( hostIP ) ) { + DBGLOG << "Ignoring static host entry that is not an IP address: " << hostIP; + continue; + } m_StaticHosts->beginGroup( hostIP ); QString hostname = m_StaticHosts->value( STATIC_HOSTS_NAME, "INVALID" ).toString(); QString osname = m_StaticHosts->value( STATIC_HOSTS_OS_NAME, "INVALID" ).toString(); @@ -147,8 +155,6 @@ void ScanningWindow::populateStaticHosts() //Form a host object AmigaHost::HardwareType hardwareType = AmigaHost::hardwareTypeFromString( hardwareName ); - QHostAddress ipAddress; - ipAddress.setAddress( hostIP ); AmigaHost *host = new AmigaHost(99999,hostname,osname,osversion,hardwareType, ipAddress, true, this ); onNewDeviceDiscoveredSlot( QSharedPointer( host ) ); m_StaticHosts->endGroup(); diff --git a/acp/AEUtils.h b/acp/AEUtils.h index fac255a..cf1cf3c 100755 --- a/acp/AEUtils.h +++ b/acp/AEUtils.h @@ -5,12 +5,18 @@ #include #include +#if defined(_MSC_VER) +#define AE_PRETTY_FUNCTION __FUNCSIG__ +#else +#define AE_PRETTY_FUNCTION __PRETTY_FUNCTION__ +#endif + #if DEBUG > 0 -#define DBGLOG qDebug().noquote() << __PRETTY_FUNCTION__ +#define DBGLOG qDebug().noquote() << AE_PRETTY_FUNCTION #else #define DBGLOG QNoDebug() #endif -#define WARNLOG qWarning().noquote() << __PRETTY_FUNCTION__ +#define WARNLOG qWarning().noquote() << AE_PRETTY_FUNCTION QString convertFromAmigaTextEncoding( char *text ); void convertFromUTF8ToAmigaTextEncoding( QString utf8Text, char *encodedText, quint64 encodedTextLength ); diff --git a/acp/filedownloader.cpp b/acp/filedownloader.cpp index 6a61a6b..772fc35 100644 --- a/acp/filedownloader.cpp +++ b/acp/filedownloader.cpp @@ -10,8 +10,8 @@ #include #if __linux__ || __APPLE__ #include -#endif #include +#endif FileDownloader::FileDownloader( QString remoteSources, QString localDestination, QString remoteHost, QObject *parent ) : QObject{parent}, diff --git a/acp/fileuploader.cpp b/acp/fileuploader.cpp index 7cfd3d4..a4de5e9 100644 --- a/acp/fileuploader.cpp +++ b/acp/fileuploader.cpp @@ -6,9 +6,9 @@ #include #if __linux || __APPLE__ #include +#include #endif #include -#include #define DEBUG 0 #include "AEUtils.h"