diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..dfaae19 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "libraries/loop-tempo-estimator"] + path = libraries/loop-tempo-estimator + url = https://github.com/saintmatthieu/loop-tempo-estimator.git diff --git a/src/Utils.cpp b/src/Utils.cpp index 21d8a57..d319c6d 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -28,10 +28,8 @@ #include #include -// Helper to scan Android Content URIs (Tree) void scanAndroidTree(const QJniObject& context, const QJniObject& treeUri, const QJniObject& parentDocId, QStringList& results, bool recursive) { QJniEnvironment env; - QJniObject contentResolver = context.callObjectMethod("getContentResolver", "()Landroid/content/ContentResolver;"); if (env.checkAndClearExceptions()) return; @@ -45,38 +43,28 @@ void scanAndroidTree(const QJniObject& context, const QJniObject& treeUri, const QJniObject cursor = contentResolver.callObjectMethod( "query", "(Landroid/net/Uri;[Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;)Landroid/database/Cursor;", - childrenUri.object(), - nullptr, nullptr, nullptr, nullptr + childrenUri.object(), nullptr, nullptr, nullptr, nullptr ); - if (env.checkAndClearExceptions()) { - return; - } - - if (!cursor.isValid()) return; + if (env.checkAndClearExceptions() || !cursor.isValid()) return; jint colDocId = cursor.callMethod("getColumnIndex", "(Ljava/lang/String;)I", QJniObject::fromString("document_id").object()); jint colMime = cursor.callMethod("getColumnIndex", "(Ljava/lang/String;)I", QJniObject::fromString("mime_type").object()); while (cursor.callMethod("moveToNext")) { if (env.checkAndClearExceptions()) break; - QString mime = cursor.callObjectMethod("getString", "(I)Ljava/lang/String;", colMime).toString(); QString docId = cursor.callObjectMethod("getString", "(I)Ljava/lang/String;", colDocId).toString(); if (mime == "vnd.android.document/directory") { - if (recursive) { - scanAndroidTree(context, treeUri, QJniObject::fromString(docId), results, true); - } + if (recursive) scanAndroidTree(context, treeUri, QJniObject::fromString(docId), results, true); } else if (mime.startsWith("audio/") || mime == "application/ogg" || mime == "audio/x-wav") { QJniObject fileUri = QJniObject::callStaticObjectMethod( "android/provider/DocumentsContract", "buildDocumentUriUsingTree", "(Landroid/net/Uri;Ljava/lang/String;)Landroid/net/Uri;", treeUri.object(), QJniObject::fromString(docId).object() ); - if (fileUri.isValid()) { - results << fileUri.toString(); - } + if (fileUri.isValid()) results << fileUri.toString(); } } cursor.callMethod("close"); @@ -85,8 +73,7 @@ void scanAndroidTree(const QJniObject& context, const QJniObject& treeUri, const Utils::Metadata getMetadataAndroid(const QString &path) { Utils::Metadata meta; - meta.title = QFileInfo(path).fileName(); // Fallback - + meta.title = QFileInfo(path).fileName(); QJniObject retriever("android/media/MediaMetadataRetriever"); if (!retriever.isValid()) return meta; @@ -95,71 +82,36 @@ Utils::Metadata getMetadataAndroid(const QString &path) { try { if (path.startsWith("content://")) { - QJniObject uri = QJniObject::callStaticObjectMethod( - "android/net/Uri", - "parse", - "(Ljava/lang/String;)Landroid/net/Uri;", - QJniObject::fromString(path).object() - ); - - retriever.callMethod( - "setDataSource", - "(Landroid/content/Context;Landroid/net/Uri;)V", - context.object(), - uri.object() - ); + QJniObject uri = QJniObject::callStaticObjectMethod("android/net/Uri", "parse", "(Ljava/lang/String;)Landroid/net/Uri;", QJniObject::fromString(path).object()); + retriever.callMethod("setDataSource", "(Landroid/content/Context;Landroid/net/Uri;)V", context.object(), uri.object()); } else { - retriever.callMethod( - "setDataSource", - "(Ljava/lang/String;)V", - QJniObject::fromString(path).object() - ); + retriever.callMethod("setDataSource", "(Ljava/lang/String;)V", QJniObject::fromString(path).object()); } - } catch (...) { - env.checkAndClearExceptions(); - return meta; - } + } catch (...) { env.checkAndClearExceptions(); return meta; } if (env.checkAndClearExceptions()) return meta; auto extract = [&](int key) -> QString { - QJniObject val = retriever.callObjectMethod( - "extractMetadata", - "(I)Ljava/lang/String;", - key - ); + QJniObject val = retriever.callObjectMethod("extractMetadata", "(I)Ljava/lang/String;", key); if (env.checkAndClearExceptions()) return QString(); return val.isValid() ? val.toString() : QString(); }; - // METADATA_KEY_TITLE = 7 - QString t = extract(7); - if (!t.isEmpty()) meta.title = t; - - // METADATA_KEY_ARTIST = 2 - QString a = extract(2); - if (!a.isEmpty()) meta.artist = a; - - // METADATA_KEY_ALBUM = 1 - QString al = extract(1); - if (!al.isEmpty()) meta.album = al; - - // METADATA_KEY_CD_TRACK_NUMBER = 0 - QString tr = extract(0); - if (!tr.isEmpty()) meta.trackNumber = tr.split('/').first().toInt(); + QString t = extract(7); if (!t.isEmpty()) meta.title = t; + QString a = extract(2); if (!a.isEmpty()) meta.artist = a; + QString al = extract(1); if (!al.isEmpty()) meta.album = al; + QString tr = extract(0); if (!tr.isEmpty()) meta.trackNumber = tr.split('/').first().toInt(); QJniObject artObj = retriever.callObjectMethod("getEmbeddedPicture", "()[B"); if (!env.checkAndClearExceptions() && artObj.isValid()) { jbyteArray jBa = artObj.object(); if (jBa) { int len = env->GetArrayLength(jBa); - QByteArray ba; - ba.resize(len); + QByteArray ba; ba.resize(len); env->GetByteArrayRegion(jBa, 0, len, reinterpret_cast(ba.data())); meta.art.loadFromData(ba); } } - retriever.callMethod("release"); env.checkAndClearExceptions(); return meta; @@ -167,26 +119,18 @@ Utils::Metadata getMetadataAndroid(const QString &path) { #endif #ifdef Q_OS_IOS -// Native iOS Metadata Extraction Utils::Metadata getMetadataIOS(const QString &path) { Utils::Metadata meta; meta.title = QFileInfo(path).fileName(); - NSURL *url = [NSURL fileURLWithPath:path.toNSString()]; AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:nil]; - NSArray *metadata = [asset commonMetadata]; - for (AVMetadataItem *item in metadata) { if (item.value == nil) continue; - - if ([item.commonKey isEqualToString:AVMetadataCommonKeyTitle]) { - meta.title = QString::fromNSString((NSString *)item.value); - } else if ([item.commonKey isEqualToString:AVMetadataCommonKeyArtist]) { - meta.artist = QString::fromNSString((NSString *)item.value); - } else if ([item.commonKey isEqualToString:AVMetadataCommonKeyAlbumName]) { - meta.album = QString::fromNSString((NSString *)item.value); - } else if ([item.commonKey isEqualToString:AVMetadataCommonKeyArtwork]) { + if ([item.commonKey isEqualToString:AVMetadataCommonKeyTitle]) meta.title = QString::fromNSString((NSString *)item.value); + else if ([item.commonKey isEqualToString:AVMetadataCommonKeyArtist]) meta.artist = QString::fromNSString((NSString *)item.value); + else if ([item.commonKey isEqualToString:AVMetadataCommonKeyAlbumName]) meta.album = QString::fromNSString((NSString *)item.value); + else if ([item.commonKey isEqualToString:AVMetadataCommonKeyArtwork]) { if ([item.value isKindOfClass:[NSData class]]) { NSData *data = (NSData *)item.value; meta.art.loadFromData(QByteArray::fromRawData((const char *)data.bytes, data.length)); @@ -196,67 +140,32 @@ Utils::Metadata getMetadataIOS(const QString &path) { return meta; } -// Native iOS File Picker Delegate @interface FilePickerDelegate : NSObject @property (nonatomic, assign) std::function callback; @property (nonatomic, assign) bool isFolder; @end - @implementation FilePickerDelegate - (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray *)urls { if (urls.count > 0) { NSURL *url = urls.firstObject; - if (self.isFolder) { - [url startAccessingSecurityScopedResource]; - } - if (self.callback) { - self.callback(QString::fromNSString(url.absoluteString)); - } + if (self.isFolder) [url startAccessingSecurityScopedResource]; + if (self.callback) self.callback(QString::fromNSString(url.absoluteString)); } } -- (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)controller { -} +- (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)controller {} @end - static FilePickerDelegate* g_pickerDelegate = nil; namespace Utils { void openIosPicker(bool folder, std::function callback) { - if (!g_pickerDelegate) { - g_pickerDelegate = [[FilePickerDelegate alloc] init]; - } + if (!g_pickerDelegate) g_pickerDelegate = [[FilePickerDelegate alloc] init]; g_pickerDelegate.callback = callback; g_pickerDelegate.isFolder = folder; - - UIDocumentPickerViewController *picker = nil; - - if (folder) { - picker = [[UIDocumentPickerViewController alloc] initForOpeningContentTypes:@[UTTypeFolder] asCopy:NO]; - } else { - picker = [[UIDocumentPickerViewController alloc] initForOpeningContentTypes:@[UTTypeAudio] asCopy:YES]; - } - + UIDocumentPickerViewController *picker = [[UIDocumentPickerViewController alloc] initForOpeningContentTypes:folder ? @[UTTypeFolder] : @[UTTypeAudio] asCopy:!folder]; picker.delegate = g_pickerDelegate; picker.allowsMultipleSelection = NO; - - UIViewController *root = nil; - for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) { - if (scene.activationState == UISceneActivationStateForegroundActive && [scene isKindOfClass:[UIWindowScene class]]) { - root = ((UIWindowScene *)scene).windows.firstObject.rootViewController; - if (root) break; - } - } - - if (!root) { - #pragma clang diagnostic push - #pragma clang diagnostic ignored "-Wdeprecated-declarations" - root = [UIApplication sharedApplication].keyWindow.rootViewController; - #pragma clang diagnostic pop - } - - if (root) { - [root presentViewController:picker animated:YES completion:nil]; - } + UIViewController *root = [UIApplication sharedApplication].keyWindow.rootViewController; + if (root) [root presentViewController:picker animated:YES completion:nil]; } } #endif @@ -267,11 +176,8 @@ void configureIOSAudioSession() { #ifdef Q_OS_IOS NSError *error = nil; AVAudioSession *session = [AVAudioSession sharedInstance]; - // Critical for background audio playback [session setCategory:AVAudioSessionCategoryPlayback error:&error]; - if (error) { - qWarning() << "Failed to set audio session category:" << QString::fromNSString(error.localizedDescription); - } + if (error) qWarning() << "Failed to set audio session category:" << QString::fromNSString(error.localizedDescription); [session setActive:YES error:&error]; #endif } @@ -279,16 +185,8 @@ void configureIOSAudioSession() { static QString getBinary(const QString& name) { QString bin = QStandardPaths::findExecutable(name); if (!bin.isEmpty()) return bin; - - QStringList paths = { - "/opt/homebrew/bin/" + name, - "/usr/local/bin/" + name, - "/usr/bin/" + name, - "/bin/" + name - }; - for (const auto& p : paths) { - if (QFile::exists(p)) return p; - } + QStringList paths = { "/opt/homebrew/bin/" + name, "/usr/local/bin/" + name, "/usr/bin/" + name, "/bin/" + name }; + for (const auto& p : paths) if (QFile::exists(p)) return p; return name; } @@ -308,87 +206,55 @@ QString convertToWav(const QString &inputPath) { #else QString wavPath = inputPath + ".temp.wav"; if (QFile::exists(wavPath)) QFile::remove(wavPath); - QProcess p; p.start(getBinary("ffmpeg"), {"-y", "-v", "quiet", "-i", inputPath, "-vn", "-f", "wav", wavPath}); - if (p.waitForFinished() && p.exitCode() == 0) { - return wavPath; - } + if (p.waitForFinished() && p.exitCode() == 0) return wavPath; return QString(); #endif } QString resolvePath(const QString& rawPath) { if (rawPath.startsWith("content://")) return rawPath; - if (rawPath.startsWith("file://")) { QUrl url(rawPath); if (url.isLocalFile()) return url.toLocalFile(); - // Fallback for malformed file:// URIs return QUrl::fromPercentEncoding(rawPath.toUtf8()).mid(7); } - - // It's a local path, return as is (spaces are fine in QString paths) return rawPath; } -// Global Runtime Cache for Album Art static QMap g_artCache; static QMutex g_cacheMutex; Metadata getMetadata(const QString &filePath) { Metadata meta; - #ifdef Q_OS_ANDROID meta = getMetadataAndroid(filePath); #elif defined(Q_OS_IOS) meta = getMetadataIOS(filePath); #else meta.title = QFileInfo(filePath).fileName(); - QString ffprobe = getBinary("ffprobe"); QString ffmpeg = getBinary("ffmpeg"); - - // 1. Get Tags (Fast) QProcess p; p.start(ffprobe, {"-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", filePath}); if (p.waitForFinished()) { QJsonDocument doc = QJsonDocument::fromJson(p.readAllStandardOutput()); - QJsonObject root = doc.object(); - QJsonObject format = root["format"].toObject(); - QJsonObject tags = format["tags"].toObject(); - + QJsonObject tags = doc.object()["format"].toObject()["tags"].toObject(); if (tags.contains("title")) meta.title = tags["title"].toString(); if (tags.contains("artist")) meta.artist = tags["artist"].toString(); if (tags.contains("album")) meta.album = tags["album"].toString(); - if (tags.contains("track")) { - meta.trackNumber = tags["track"].toString().split('/').first().toInt(); - } + if (tags.contains("track")) meta.trackNumber = tags["track"].toString().split('/').first().toInt(); } - - // 2. Check Caches for Album Art if (!meta.album.isEmpty()) { QMutexLocker locker(&g_cacheMutex); - - // Runtime Cache - if (g_artCache.contains(meta.album)) { - meta.art = g_artCache[meta.album]; - } else { - // Disk Cache + if (g_artCache.contains(meta.album)) meta.art = g_artCache[meta.album]; + else { QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/covers"; - QDir().mkpath(cacheDir); QString hash = QString(QCryptographicHash::hash(meta.album.toUtf8(), QCryptographicHash::Md5).toHex()); - QString cachePath = cacheDir + "/" + hash + ".png"; - - if (QFile::exists(cachePath)) { - if (meta.art.load(cachePath)) { - g_artCache.insert(meta.album, meta.art); - } - } + if (QFile::exists(cacheDir + "/" + hash + ".png") && meta.art.load(cacheDir + "/" + hash + ".png")) g_artCache.insert(meta.album, meta.art); } } - - // 3. Extract Art (Slow) if not found if (meta.art.isNull()) { QProcess pArt; pArt.start(ffmpeg, {"-y", "-v", "quiet", "-i", filePath, "-an", "-vcodec", "png", "-f", "image2pipe", "-"}); @@ -396,13 +262,11 @@ Metadata getMetadata(const QString &filePath) { QByteArray data = pArt.readAllStandardOutput(); if (!data.isEmpty()) { meta.art.loadFromData(data); - - // Update Caches - if (!meta.album.isEmpty() && !meta.art.isNull()) { + if (!meta.album.isEmpty()) { QMutexLocker locker(&g_cacheMutex); g_artCache.insert(meta.album, meta.art); - QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/covers"; + QDir().mkpath(cacheDir); QString hash = QString(QCryptographicHash::hash(meta.album.toUtf8(), QCryptographicHash::Md5).toHex()); meta.art.save(cacheDir + "/" + hash + ".png", "PNG"); } @@ -410,19 +274,13 @@ Metadata getMetadata(const QString &filePath) { } } #endif - - // Generate Thumbnail for Playlist Performance - if (!meta.art.isNull()) { - meta.thumbnail = QPixmap::fromImage(meta.art.scaled(60, 60, Qt::KeepAspectRatio, Qt::SmoothTransformation)); - } - + if (!meta.art.isNull()) meta.thumbnail = QPixmap::fromImage(meta.art.scaled(60, 60, Qt::KeepAspectRatio, Qt::SmoothTransformation)); return meta; } QVector extractAlbumColors(const QImage &art, int numBins) { QVector palette(numBins, QColor(127, 127, 127)); if (art.isNull()) return palette; - QImage scaled = art.scaled(numBins, 20, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); for (int x = 0; x < numBins; ++x) { float maxVibrancy = -1.0f; @@ -431,10 +289,7 @@ QVector extractAlbumColors(const QImage &art, int numBins) { QColor c = scaled.pixelColor(x, y); float s = c.hsvSaturationF(); float v = c.valueF(); - if (s * v > maxVibrancy) { - maxVibrancy = s * v; - bestColor = c; - } + if (s * v > maxVibrancy) { maxVibrancy = s * v; bestColor = c; } } palette[x] = bestColor; } @@ -445,76 +300,35 @@ bool isContentUriFolder(const QString& path) { #ifdef Q_OS_ANDROID if (!path.startsWith("content://")) return false; QJniEnvironment env; - QJniObject uri = QJniObject::callStaticObjectMethod( - "android/net/Uri", "parse", "(Ljava/lang/String;)Landroid/net/Uri;", - QJniObject::fromString(path).object() - ); + QJniObject uri = QJniObject::callStaticObjectMethod("android/net/Uri", "parse", "(Ljava/lang/String;)Landroid/net/Uri;", QJniObject::fromString(path).object()); if (!uri.isValid()) return false; - QJniObject context = QNativeInterface::QAndroidApplication::context(); QJniObject contentResolver = context.callObjectMethod("getContentResolver", "()Landroid/content/ContentResolver;"); - QJniObject type = contentResolver.callObjectMethod( - "getType", "(Landroid/net/Uri;)Ljava/lang/String;", uri.object() - ); - - if (env.checkAndClearExceptions()) return false; - if (!type.isValid()) return false; - - QString mime = type.toString(); - return mime == "vnd.android.document/directory"; + QJniObject type = contentResolver.callObjectMethod("getType", "(Landroid/net/Uri;)Ljava/lang/String;", uri.object()); + if (env.checkAndClearExceptions() || !type.isValid()) return false; + return type.toString() == "vnd.android.document/directory"; #else return false; #endif } QStringList scanDirectory(const QString &path, bool recursive) { - #ifdef Q_OS_ANDROID +#ifdef Q_OS_ANDROID if (path.startsWith("content://")) { QStringList results; QJniEnvironment env; - QJniObject uri = QJniObject::callStaticObjectMethod( - "android/net/Uri", "parse", "(Ljava/lang/String;)Landroid/net/Uri;", - QJniObject::fromString(path).object() - ); + QJniObject uri = QJniObject::callStaticObjectMethod("android/net/Uri", "parse", "(Ljava/lang/String;)Landroid/net/Uri;", QJniObject::fromString(path).object()); if (!uri.isValid()) return results; - QJniObject context = QNativeInterface::QAndroidApplication::context(); QJniObject contentResolver = context.callObjectMethod("getContentResolver", "()Landroid/content/ContentResolver;"); - - contentResolver.callMethod( - "takePersistableUriPermission", - "(Landroid/net/Uri;I)V", - uri.object(), - 1 - ); - - if (env.checkAndClearExceptions()) { - } - - QJniObject docId = QJniObject::callStaticObjectMethod( - "android/provider/DocumentsContract", "getTreeDocumentId", - "(Landroid/net/Uri;)Ljava/lang/String;", uri.object() - ); - - if (env.checkAndClearExceptions() || !docId.isValid()) { - return results; - } - - QJniObject parentDocUri = QJniObject::callStaticObjectMethod( - "android/provider/DocumentsContract", "buildDocumentUriUsingTree", - "(Landroid/net/Uri;Ljava/lang/String;)Landroid/net/Uri;", - uri.object(), docId.object() - ); - - if (env.checkAndClearExceptions() || !parentDocUri.isValid()) { - return results; - } - + contentResolver.callMethod("takePersistableUriPermission", "(Landroid/net/Uri;I)V", uri.object(), 1); + if (env.checkAndClearExceptions()) {} + QJniObject docId = QJniObject::callStaticObjectMethod("android/provider/DocumentsContract", "getTreeDocumentId", "(Landroid/net/Uri;)Ljava/lang/String;", uri.object()); + if (env.checkAndClearExceptions() || !docId.isValid()) return results; scanAndroidTree(context, uri, docId, results, recursive); return results; } - #endif - +#endif QStringList files; QStringList filters = {"*.mp3", "*.m4a", "*.wav", "*.flac", "*.ogg", "*.aif*", "*.aac"}; QDirIterator::IteratorFlag flag = recursive ? QDirIterator::Subdirectories : QDirIterator::NoIteratorFlags; @@ -527,32 +341,23 @@ void requestAndroidPermissions(std::function callback) { #ifdef Q_OS_ANDROID QJniObject activity = QNativeInterface::QAndroidApplication::context(); - // Check SDK version - QJniObject version = QJniObject::getStaticField("android/os/Build$VERSION", "SDK_INT", "I"); - int sdkInt = version.callMethod("intValue"); + // FIX: Retrieve SDK_INT as a primitive jint, not a QJniObject + jint sdkInt = QJniObject::getStaticField("android/os/Build$VERSION", "SDK_INT"); + + QString permission = (sdkInt >= 33) ? "android.permission.READ_MEDIA_AUDIO" : "android.permission.READ_EXTERNAL_STORAGE"; + + jint result = activity.callMethod("checkSelfPermission", "(Ljava/lang/String;)I", QJniObject::fromString(permission).object()); + if (result == 0) callback(true); + else { + // FIX: Use QJniEnvironment to find the class, as QJniObject::findClass does not exist + QJniEnvironment env; + jclass stringClass = env.findClass("java/lang/String"); - QString permission; - if (sdkInt >= 33) { // Android 13+ - permission = "android.permission.READ_MEDIA_AUDIO"; - } else { - permission = "android.permission.READ_EXTERNAL_STORAGE"; - } - - jint result = activity.callMethod( - "checkSelfPermission", - "(Ljava/lang/String;)I", - QJniObject::fromString(permission).object() - ); - - if (result == 0) { // PERMISSION_GRANTED - callback(true); - } else { - // Request permission QJniObject permissionsArray = QJniObject::callStaticObjectMethod( "java/lang/reflect/Array", "newInstance", "(Ljava/lang/Class;I)Ljava/lang/Object;", - QJniObject::findClass("java/lang/String"), + stringClass, 1 ); @@ -564,17 +369,8 @@ void requestAndroidPermissions(std::function callback) { 0, QJniObject::fromString(permission).object() ); - - activity.callMethod( - "requestPermissions", - "([Ljava/lang/String;I)V", - permissionsArray.object(), - 101 // Request Code - ); - // We can't easily wait for the callback here without a native listener. - // Return false to indicate permission was not immediately available. - // The system dialog will show up. User has to click "Open" again. + activity.callMethod("requestPermissions", "([Ljava/lang/String;I)V", permissionsArray.object(), 101); callback(false); } #else @@ -582,10 +378,7 @@ void requestAndroidPermissions(std::function callback) { #endif } -// --- MetadataLoader Implementation --- - MetadataLoader::MetadataLoader(QObject* parent) : QObject(parent) {} - void MetadataLoader::startLoading(const QStringList& paths) { m_stop = false; for (int i = 0; i < paths.size(); ++i) { @@ -595,9 +388,6 @@ void MetadataLoader::startLoading(const QStringList& paths) { } emit finished(); } - -void MetadataLoader::stop() { - m_stop = true; -} +void MetadataLoader::stop() { m_stop = true; } } \ No newline at end of file diff --git a/windows/build_arm64.bat b/windows/build_arm64.bat index 3bfb682..1f1d763 100644 --- a/windows/build_arm64.bat +++ b/windows/build_arm64.bat @@ -32,7 +32,22 @@ set "VCPKG_EXE=!VS_INSTALL_DIR!\VC\vcpkg\vcpkg.exe" set "VCPKG_CMAKE=!VS_INSTALL_DIR!\VC\vcpkg\scripts\buildsystems\vcpkg.cmake" :: ============================================================================== -:: 1. CHECK VCPKG (Crucial Step Restored) +:: 0. MANIFEST GENERATION +:: ============================================================================== +echo [INFO] Generating vcpkg.json... +( + echo { + echo "name": "yrcrystals", + echo "version-string": "1.0.0", + echo "builtin-baseline": "b1b19307e2d2ec1eefbdb7ea069de7d4bcd31f01", + echo "dependencies": [ + echo "fftw3" + echo ] + echo } +) > "%PROJECT_ROOT%\vcpkg.json" + +:: ============================================================================== +:: 1. CHECK VCPKG :: ============================================================================== echo [INFO] Verifying dependencies... cd "%PROJECT_ROOT%" diff --git a/windows/build_x64.bat b/windows/build_x64.bat index b4094f5..e1ed842 100644 --- a/windows/build_x64.bat +++ b/windows/build_x64.bat @@ -2,64 +2,223 @@ setlocal enabledelayedexpansion :: ============================================================================== -:: CONFIGURATION +:: PATHS :: ============================================================================== -set "BUILD_DIR=..\build_windows\x64" -set "QT_PATH=C:\Qt\6.8.3\msvc2022_64" +set "SCRIPT_DIR=%~dp0" +set "PROJECT_ROOT=%SCRIPT_DIR%.." +set "BUILD_DIR=%PROJECT_ROOT%\build_windows\x64" +set "QT_X64_BIN=C:\Qt\6.8.3\msvc2022_64\bin" +set "QT_X64_PLUGINS=C:\Qt\6.8.3\msvc2022_64\plugins" +set "VCPKG_BIN=%PROJECT_ROOT%\vcpkg_installed\x64-windows\bin" :: ============================================================================== -:: AUTO-DETECT VISUAL STUDIO +:: ENVIRONMENT SETUP (VS Discovery) :: ============================================================================== -set "VSWHERE=%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -if not exist "!VSWHERE!" set "VSWHERE=%ProgramFiles%\Microsoft Visual Studio\Installer\vswhere.exe" - -if exist "!VSWHERE!" ( - for /f "usebackq tokens=*" %%i in (`"!VSWHERE!" -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`) do ( - set "VS_INSTALL_DIR=%%i" +if defined VSINSTALLDIR ( + set "VS_INSTALL_DIR=!VSINSTALLDIR!" +) else ( + if exist "C:\Program Files\Microsoft Visual Studio\2022\Preview\VC\Auxiliary\Build\vcvarsall.bat" ( + set "VS_INSTALL_DIR=C:\Program Files\Microsoft Visual Studio\2022\Preview" + ) else if exist "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" ( + set "VS_INSTALL_DIR=C:\Program Files\Microsoft Visual Studio\2022\Enterprise" + ) else if exist "C:\Program Files\Microsoft Visual Studio\18\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" ( + set "VS_INSTALL_DIR=C:\Program Files\Microsoft Visual Studio\18\Enterprise" ) ) -if not defined VS_INSTALL_DIR ( - echo [ERROR] Could not find Visual Studio. - pause - exit /b 1 -) - +if "!VS_INSTALL_DIR:~-1!"=="\" set "VS_INSTALL_DIR=!VS_INSTALL_DIR:~0,-1!" set "VCVARSALL=!VS_INSTALL_DIR!\VC\Auxiliary\Build\vcvarsall.bat" +set "VCPKG_EXE=!VS_INSTALL_DIR!\VC\vcpkg\vcpkg.exe" set "VCPKG_CMAKE=!VS_INSTALL_DIR!\VC\vcpkg\scripts\buildsystems\vcpkg.cmake" -echo [INFO] Found Visual Studio at: !VS_INSTALL_DIR! :: ============================================================================== -:: COMPILER SETUP (Cross-Compile) +:: 0. DYNAMIC BASELINE DISCOVERY :: ============================================================================== -if /i "%PROCESSOR_ARCHITECTURE%"=="ARM64" ( - echo [INFO] Host is ARM64. Using ARM64_x64 cross-compiler. - set "VCVARS_ARCH=arm64_x64" -) else ( - echo [INFO] Host is x64. Using Native x64 compiler. - set "VCVARS_ARCH=x64" +echo [INFO] Probing for latest vcpkg baseline... + +:: 1. Create temporary manifest with empty baseline to provoke the "suggestion" error +( + echo { + echo "name": "yrcrystals", + echo "version-string": "1.0.0", + echo "dependencies": [ "fftw3" ], + echo "builtin-baseline": "" + echo } +) > "%PROJECT_ROOT%\vcpkg.json" + +:: 2. Run dry-run and capture output (stderr included) +"!VCPKG_EXE!" install --triplet x64-windows --dry-run > "%PROJECT_ROOT%\vcpkg_probe.log" 2>&1 + +:: 3. Extract the hash using PowerShell regex +:: Matches pattern: "builtin-baseline": "HEX_HASH" +set "DETECTED_HASH=" +for /f "usebackq tokens=*" %%H in (`powershell -NoProfile -Command "$content = Get-Content '%PROJECT_ROOT%\vcpkg_probe.log' -Raw; if ($content -match 'builtin-baseline.: .([a-f0-9]+).') { $matches[1] }"`) do ( + set "DETECTED_HASH=%%H" ) -call "!VCVARSALL!" !VCVARS_ARCH! +if "!DETECTED_HASH!"=="" ( + echo [WARNING] Could not auto-detect baseline. Defaulting to standard hash. + set "DETECTED_HASH=b1b19307e2d2ec1eefbdb7ea069de7d4bcd31f01" +) else ( + echo [INFO] Detected Baseline: !DETECTED_HASH! +) + +:: Clean up log +del "%PROJECT_ROOT%\vcpkg_probe.log" 2>nul + +:: 4. Generate Final Manifest with discovered hash +echo [INFO] Writing vcpkg.json... +( + echo { + echo "name": "yrcrystals", + echo "version-string": "1.0.0", + echo "builtin-baseline": "!DETECTED_HASH!", + echo "dependencies": [ + echo "fftw3" + echo ] + echo } +) > "%PROJECT_ROOT%\vcpkg.json" :: ============================================================================== -:: BUILD +:: 1. INSTALL DEPENDENCIES (Targeting x64-windows) :: ============================================================================== +echo [INFO] Verifying dependencies for x64... +cd "%PROJECT_ROOT%" +"!VCPKG_EXE!" install --triplet x64-windows + +if %errorlevel% neq 0 ( + echo [ERROR] VCPKG install failed. + pause + exit /b %errorlevel% +) + +:: ============================================================================== +:: 2. AUTO-DETECT MAGICK +:: ============================================================================== +set "MAGICK_PATH=" +where magick >nul 2>nul +if %errorlevel% equ 0 ( + for /f "delims=" %%i in ('where magick') do set "MAGICK_PATH=%%i" + echo [INFO] Found Magick in PATH. +) +if not defined MAGICK_PATH ( + for /d %%d in ("C:\Program Files\ImageMagick*") do ( + if exist "%%d\magick.exe" set "MAGICK_PATH=%%d\magick.exe" + ) +) +if defined MAGICK_PATH ( + echo [INFO] Using ImageMagick: !MAGICK_PATH! + set "CMAKE_MAGICK_ARG=-DMAGICK_EXECUTABLE="!MAGICK_PATH!"" +) else ( + echo [WARNING] ImageMagick not found. Icons will be skipped. + set "CMAKE_MAGICK_ARG=" +) + +:: ============================================================================== +:: 3. CONFIGURE & BUILD +:: ============================================================================== +echo [INFO] Setting up Environment for x64 Build... +:: CRITICAL: arm64_x64 means "Use ARM64 host to build x64 binaries" +call "!VCVARSALL!" arm64_x64 + if not exist "%BUILD_DIR%" mkdir "%BUILD_DIR%" cd "%BUILD_DIR%" -echo [INFO] Configuring for x64... -cmake -G "Ninja" ^ - -DCMAKE_BUILD_TYPE=Release ^ - -DCMAKE_PREFIX_PATH="%QT_PATH%" ^ - -DCMAKE_TOOLCHAIN_FILE="!VCPKG_CMAKE!" ^ - ..\.. +set "NEED_CONFIG=0" +if not exist "build.ninja" set "NEED_CONFIG=1" +if not exist "CMakeCache.txt" set "NEED_CONFIG=1" -if %errorlevel% neq 0 pause && exit /b %errorlevel% +if "!NEED_CONFIG!"=="1" ( + echo [INFO] Build configuration missing. Running CMake Configure... + cmake -G "Ninja" ^ + -DCMAKE_BUILD_TYPE=Release ^ + -DCMAKE_PREFIX_PATH="%QT_X64_BIN%\..;%PROJECT_ROOT%\vcpkg_installed\x64-windows" ^ + -DCMAKE_TOOLCHAIN_FILE="!VCPKG_CMAKE!" ^ + -DVCPKG_TARGET_TRIPLET=x64-windows ^ + !CMAKE_MAGICK_ARG! ^ + "%PROJECT_ROOT%" + + if !errorlevel! neq 0 ( + echo [ERROR] Configuration Failed. + pause + exit /b !errorlevel! + ) +) echo [INFO] Building... cmake --build . +if %errorlevel% neq 0 ( + echo [ERROR] Build Failed. + pause + exit /b %errorlevel% +) + +:: ============================================================================== +:: 4. THE NUKE (Clean Slate) +:: ============================================================================== echo. -echo [SUCCESS] x64 Build located in build_windows\x64 +echo [CLEAN] Removing old DLLs and Plugins... +del /f /q *.dll 2>nul +if exist "platforms" rmdir /s /q "platforms" +if exist "styles" rmdir /s /q "styles" +if exist "multimedia" rmdir /s /q "multimedia" +if exist "audio" rmdir /s /q "audio" +if exist "imageformats" rmdir /s /q "imageformats" +if exist "iconengines" rmdir /s /q "iconengines" +if exist "tls" rmdir /s /q "tls" + +:: ============================================================================== +:: 5. COPY DEPENDENCIES (x64 Source) +:: ============================================================================== +echo. +echo [COPY] Copying DLLs... + +:: Core & Helpers +copy /Y "%QT_X64_BIN%\Qt6Core.dll" . >nul +copy /Y "%QT_X64_BIN%\Qt6Gui.dll" . >nul +copy /Y "%QT_X64_BIN%\Qt6Widgets.dll" . >nul +copy /Y "%QT_X64_BIN%\Qt6Multimedia.dll" . >nul +copy /Y "%QT_X64_BIN%\Qt6OpenGL.dll" . >nul +copy /Y "%QT_X64_BIN%\Qt6OpenGLWidgets.dll" . >nul +copy /Y "%QT_X64_BIN%\Qt6Network.dll" . >nul +copy /Y "%QT_X64_BIN%\Qt6Svg.dll" . >nul +copy /Y "%QT_X64_BIN%\Qt6ShaderTools.dll" . >nul +copy /Y "%QT_X64_BIN%\Qt6Concurrent.dll" . >nul +copy /Y "%QT_X64_BIN%\d3dcompiler_47.dll" . >nul +copy /Y "%QT_X64_BIN%\opengl32sw.dll" . >nul + +:: FFmpeg +copy /Y "%QT_X64_BIN%\avcodec*.dll" . >nul +copy /Y "%QT_X64_BIN%\avformat*.dll" . >nul +copy /Y "%QT_X64_BIN%\avutil*.dll" . >nul +copy /Y "%QT_X64_BIN%\swresample*.dll" . >nul +copy /Y "%QT_X64_BIN%\swscale*.dll" . >nul + +:: Plugins +if not exist "platforms" mkdir "platforms" +copy /Y "%QT_X64_PLUGINS%\platforms\qwindows.dll" "platforms\" >nul + +if not exist "styles" mkdir "styles" +copy /Y "%QT_X64_PLUGINS%\styles\qwindowsvistastyle.dll" "styles\" >nul + +if not exist "imageformats" mkdir "imageformats" +copy /Y "%QT_X64_PLUGINS%\imageformats\*.dll" "imageformats\" >nul + +if not exist "multimedia" mkdir "multimedia" +copy /Y "%QT_X64_PLUGINS%\multimedia\*.dll" "multimedia\" >nul + +if not exist "iconengines" mkdir "iconengines" +copy /Y "%QT_X64_PLUGINS%\iconengines\*.dll" "iconengines\" >nul + +if not exist "tls" mkdir "tls" +copy /Y "%QT_X64_PLUGINS%\tls\*.dll" "tls\" >nul + +:: FFTW3 (x64) +if exist "%VCPKG_BIN%\fftw3.dll" ( + copy /Y "%VCPKG_BIN%\fftw3.dll" . >nul +) + +echo. +echo [SUCCESS] x64 Build Complete. pause \ No newline at end of file