Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4683db900 | ||
|
|
5a90643ba8 | ||
|
|
4a1ab2f004 | ||
|
|
36be755a78 | ||
|
|
ffd8c88b1f | ||
|
|
84ff163b34 | ||
|
|
811e625618 | ||
|
|
037b0d7dea | ||
|
|
d578d3c66d | ||
|
|
2644dbd5ab | ||
|
|
245103dc30 |
@@ -130,10 +130,7 @@ InsertBraces: false
|
|||||||
InsertTrailingCommas: None
|
InsertTrailingCommas: None
|
||||||
JavaScriptQuotes: Leave
|
JavaScriptQuotes: Leave
|
||||||
JavaScriptWrapImports: true
|
JavaScriptWrapImports: true
|
||||||
KeepEmptyLines:
|
KeepEmptyLinesAtTheStartOfBlocks: true
|
||||||
AtEndOfFile: true
|
|
||||||
AtStartOfBlock: true
|
|
||||||
AtStartOfFile: true
|
|
||||||
LambdaBodyIndentation: Signature
|
LambdaBodyIndentation: Signature
|
||||||
MacroBlockBegin: ''
|
MacroBlockBegin: ''
|
||||||
MacroBlockEnd: ''
|
MacroBlockEnd: ''
|
||||||
|
|||||||
4
.github/FUNDING.yml
vendored
Normal file
4
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
github: jonaski
|
||||||
|
patreon: jonaskvinge
|
||||||
|
ko_fi: jonaskvinge
|
||||||
|
custom: https://paypal.me/jonaskvinge
|
||||||
1
.github/ISSUE_TEMPLATE/config.yml
vendored
1
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1 +0,0 @@
|
|||||||
blank_issues_enabled: false
|
|
||||||
543
.github/workflows/build.yml
vendored
543
.github/workflows/build.yml
vendored
File diff suppressed because it is too large
Load Diff
37
.gitignore
vendored
37
.gitignore
vendored
@@ -1,6 +1,6 @@
|
|||||||
|
/build
|
||||||
/bin
|
/bin
|
||||||
/CMakeLists.txt.user
|
/CMakeLists.txt.user
|
||||||
/.qtcreator
|
|
||||||
/.kdev4
|
/.kdev4
|
||||||
/strawberry.kdev4
|
/strawberry.kdev4
|
||||||
/.vscode
|
/.vscode
|
||||||
@@ -11,35 +11,8 @@
|
|||||||
/out
|
/out
|
||||||
/CMakeSettings.json
|
/CMakeSettings.json
|
||||||
/dist/scripts/maketarball.sh
|
/dist/scripts/maketarball.sh
|
||||||
|
/dist/unix/strawberry.spec
|
||||||
|
/dist/windows/strawberry.nsi
|
||||||
|
/debian/control
|
||||||
/debian/changelog
|
/debian/changelog
|
||||||
_codeql_detected_source_root
|
/dist/macos/Info.plist
|
||||||
|
|
||||||
# Build output (keep build tooling scripts in /build_tools/ tracked)
|
|
||||||
/cmake-build*/
|
|
||||||
/build*/
|
|
||||||
!/build_tools/
|
|
||||||
!/build_tools/**
|
|
||||||
|
|
||||||
# macOS noise
|
|
||||||
.DS_Store
|
|
||||||
/bin
|
|
||||||
/CMakeLists.txt.user
|
|
||||||
/.qtcreator
|
|
||||||
/.kdev4
|
|
||||||
/strawberry.kdev4
|
|
||||||
/.vscode
|
|
||||||
/.code-workspace
|
|
||||||
/.sublime-workspace
|
|
||||||
/.idea
|
|
||||||
/.vs
|
|
||||||
/out
|
|
||||||
/CMakeSettings.json
|
|
||||||
/dist/scripts/maketarball.sh
|
|
||||||
/debian/changelog
|
|
||||||
_codeql_detected_source_root
|
|
||||||
|
|
||||||
# Build output (keep build tooling scripts in /build_tools/ tracked)
|
|
||||||
/cmake-build*/
|
|
||||||
/build*/
|
|
||||||
!/build_tools/
|
|
||||||
!/build_tools/**
|
|
||||||
|
|||||||
4
.gitmodules
vendored
4
.gitmodules
vendored
@@ -0,0 +1,4 @@
|
|||||||
|
[submodule "3rdparty/kdsingleapplication/KDSingleApplication"]
|
||||||
|
path = 3rdparty/kdsingleapplication/KDSingleApplication
|
||||||
|
url = https://github.com/KDAB/KDSingleApplication.git
|
||||||
|
branch = master
|
||||||
|
|||||||
28
3rdparty/README.md
vendored
Normal file
28
3rdparty/README.md
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
3rdparty libraries located in this directory
|
||||||
|
============================================
|
||||||
|
|
||||||
|
KDSingleApplication
|
||||||
|
-------------------
|
||||||
|
A small library used by Strawberry to prevent it from starting twice per user session.
|
||||||
|
If the user tries to start strawberry twice, the main window will maximize instead of starting another instance.
|
||||||
|
It is also used to pass command-line options through to the first instance.
|
||||||
|
This 3rdparty copy is used only if KDSingleApplication 1.1 or higher is not found on the system.
|
||||||
|
|
||||||
|
URL: https://github.com/KDAB/KDSingleApplication/
|
||||||
|
|
||||||
|
|
||||||
|
SPMediaKeyTap
|
||||||
|
-------------
|
||||||
|
A library used on macOS to exclusively grab global media shortcuts.
|
||||||
|
|
||||||
|
The library is no longer maintained by the original author.
|
||||||
|
|
||||||
|
The directory can safely be deleted on other platforms.
|
||||||
|
|
||||||
|
getopt
|
||||||
|
------
|
||||||
|
getopt included on Windows for command line options parsing with Unicode support .
|
||||||
|
|
||||||
|
The directory can safely be deleted on other platforms.
|
||||||
|
|
||||||
|
URL: https://github.com/ludvikjerabek/getopt-win
|
||||||
11
3rdparty/SPMediaKeyTap/CMakeLists.txt
vendored
Normal file
11
3rdparty/SPMediaKeyTap/CMakeLists.txt
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
set(SPMEDIAKEY-SOURCES
|
||||||
|
SPMediaKeyTap.m
|
||||||
|
)
|
||||||
|
|
||||||
|
set(SPMEDIAKEY-HEADERS
|
||||||
|
SPMediaKeyTap.h
|
||||||
|
)
|
||||||
|
|
||||||
|
ADD_LIBRARY(SPMediaKeyTap STATIC
|
||||||
|
${SPMEDIAKEY-SOURCES}
|
||||||
|
)
|
||||||
8
3rdparty/SPMediaKeyTap/LICENSE
vendored
Normal file
8
3rdparty/SPMediaKeyTap/LICENSE
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
Copyright (c) 2011, Joachim Bengtsson
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Neither the name of the organization nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
12
3rdparty/SPMediaKeyTap/README.md
vendored
Normal file
12
3rdparty/SPMediaKeyTap/README.md
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
SPMediaKeyTap
|
||||||
|
=============
|
||||||
|
|
||||||
|
`SPMediaKeyTap` abstracts a `CGEventHook` and other nastiness in order to give you a relatively simple API to receive media key events (prev/next/playpause, on F7 to F9 on modern MacBook Pros) exclusively, without them reaching other applications like iTunes. `SPMediaKeyTap` is clever enough to resign its exclusive lock on media keys by looking for which application was active most recently: if that application is in `SPMediaKeyTap`'s whitelist, it will resign the keys. This is similar to the behavior of Apple's applications collaborating on media key handling exclusivity, but unfortunately, Apple is not exposing any APIs allowing third-parties to join in on this collaboration.
|
||||||
|
|
||||||
|
For now, the whitelist is just a hardcoded array in `+[SPMediaKeyTap defaultMediaKeyUserBundleIdentifiers]`. If your app starts using `SPMediaKeyTap`, please [mail me](mailto:nevyn@spotify.com) your bundle ID, and I'll include it in the canonical repository. This is a bad solution; a better solution would be to use distributed notifications to collaborate in creating this whitelist at runtime. Hopefully someone'll have the time and energy to write this soon.
|
||||||
|
|
||||||
|
In `Example/SPMediaKeyTapExampleAppDelegate.m` is an example of both how you use `SPMediaKeyTap`, and how you handle the semi-private `NSEvent` subtypes involved in media keys, including on how to fall back to non-event tap handling of these events.
|
||||||
|
|
||||||
|
`SPMediaKeyTap` and other `CGEventHook`s on the event type `NSSystemDefined` is known to interfere with each other and applications doing weird stuff with mouse input, because mouse clicks are also part of the `NSSystemDefined` category. The single issue we have had reported here at Spotify is Adobe Fireworks, in which item selection stops working with `SPMediaKeyTap` is active.
|
||||||
|
|
||||||
|
`SPMediaKeyTap` requires 10.5 to work, and disables itself on 10.4.
|
||||||
53
3rdparty/SPMediaKeyTap/SPMediaKeyTap.h
vendored
Normal file
53
3rdparty/SPMediaKeyTap/SPMediaKeyTap.h
vendored
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
Copyright (c) 2011, Joachim Bengtsson
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without modification,
|
||||||
|
are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Neither the name of the organization nor the names of its contributors may
|
||||||
|
be used to endorse or promote products derived from this software without
|
||||||
|
specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#import <Cocoa/Cocoa.h>
|
||||||
|
#import <IOKit/hidsystem/ev_keymap.h>
|
||||||
|
|
||||||
|
// http://overooped.com/post/2593597587/mediakeys
|
||||||
|
|
||||||
|
#define SPSystemDefinedEventMediaKeys 8
|
||||||
|
|
||||||
|
@interface SPMediaKeyTap : NSObject
|
||||||
|
|
||||||
|
- (id)initWithDelegate:(id)delegate;
|
||||||
|
|
||||||
|
+ (BOOL)usesGlobalMediaKeyTap;
|
||||||
|
- (BOOL)startWatchingMediaKeys;
|
||||||
|
- (void)stopWatchingMediaKeys;
|
||||||
|
- (void)handleAndReleaseMediaKeyEvent:(NSEvent *)event;
|
||||||
|
@end
|
||||||
|
|
||||||
|
@interface NSObject (SPMediaKeyTapDelegate)
|
||||||
|
- (void)mediaKeyTap:(SPMediaKeyTap*)keyTap receivedMediaKeyEvent:(NSEvent*)event;
|
||||||
|
@end
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
extern NSString *kIgnoreMediaKeysDefaultsKey;
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
361
3rdparty/SPMediaKeyTap/SPMediaKeyTap.m
vendored
Normal file
361
3rdparty/SPMediaKeyTap/SPMediaKeyTap.m
vendored
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
/*
|
||||||
|
Copyright (c) 2011, Joachim Bengtsson
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without modification,
|
||||||
|
are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Neither the name of the organization nor the names of its contributors may
|
||||||
|
be used to endorse or promote products derived from this software without
|
||||||
|
specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Copyright (c) 2010 Spotify AB
|
||||||
|
#import "SPMediaKeyTap.h"
|
||||||
|
|
||||||
|
// Define to enable app list debug output
|
||||||
|
// #define DEBUG_SPMEDIAKEY_APPLIST 1
|
||||||
|
|
||||||
|
NSString *kIgnoreMediaKeysDefaultsKey = @"SPIgnoreMediaKeys";
|
||||||
|
|
||||||
|
@interface SPMediaKeyTap () {
|
||||||
|
CFMachPortRef _eventPort;
|
||||||
|
CFRunLoopSourceRef _eventPortSource;
|
||||||
|
CFRunLoopRef _tapThreadRL;
|
||||||
|
NSThread *_tapThread;
|
||||||
|
BOOL _shouldInterceptMediaKeyEvents;
|
||||||
|
id _delegate;
|
||||||
|
// The app that is frontmost in this list owns media keys
|
||||||
|
NSMutableArray<NSRunningApplication *> *_mediaKeyAppList;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)shouldInterceptMediaKeyEvents;
|
||||||
|
- (void)setShouldInterceptMediaKeyEvents:(BOOL)newSetting;
|
||||||
|
- (void)startWatchingAppSwitching;
|
||||||
|
- (void)stopWatchingAppSwitching;
|
||||||
|
- (void)eventTapThread;
|
||||||
|
@end
|
||||||
|
|
||||||
|
static CGEventRef tapEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon);
|
||||||
|
|
||||||
|
|
||||||
|
// Inspired by http://gist.github.com/546311
|
||||||
|
|
||||||
|
@implementation SPMediaKeyTap
|
||||||
|
|
||||||
|
#pragma mark -
|
||||||
|
#pragma mark Setup and teardown
|
||||||
|
|
||||||
|
- (id)initWithDelegate:(id)delegate
|
||||||
|
{
|
||||||
|
self = [super init];
|
||||||
|
if (self) {
|
||||||
|
_delegate = delegate;
|
||||||
|
[self startWatchingAppSwitching];
|
||||||
|
_mediaKeyAppList = [NSMutableArray new];
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)dealloc
|
||||||
|
{
|
||||||
|
[self stopWatchingMediaKeys];
|
||||||
|
[self stopWatchingAppSwitching];
|
||||||
|
[super dealloc];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)startWatchingAppSwitching
|
||||||
|
{
|
||||||
|
// Listen to "app switched" event, so that we don't intercept media keys if we
|
||||||
|
// weren't the last "media key listening" app to be active
|
||||||
|
|
||||||
|
[[[NSWorkspace sharedWorkspace] notificationCenter] addObserver:self
|
||||||
|
selector:@selector(frontmostAppChanged:)
|
||||||
|
name:NSWorkspaceDidActivateApplicationNotification
|
||||||
|
object:nil];
|
||||||
|
|
||||||
|
|
||||||
|
[[[NSWorkspace sharedWorkspace] notificationCenter] addObserver:self
|
||||||
|
selector:@selector(appTerminated:)
|
||||||
|
name:NSWorkspaceDidTerminateApplicationNotification
|
||||||
|
object:nil];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)stopWatchingAppSwitching
|
||||||
|
{
|
||||||
|
[[[NSWorkspace sharedWorkspace] notificationCenter] removeObserver:self];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)startWatchingMediaKeys
|
||||||
|
{
|
||||||
|
// Prevent having multiple mediaKeys threads
|
||||||
|
[self stopWatchingMediaKeys];
|
||||||
|
|
||||||
|
[self setShouldInterceptMediaKeyEvents:YES];
|
||||||
|
|
||||||
|
// Add an event tap to intercept the system defined media key events
|
||||||
|
_eventPort = CGEventTapCreate(kCGSessionEventTap,
|
||||||
|
kCGHeadInsertEventTap,
|
||||||
|
kCGEventTapOptionDefault,
|
||||||
|
CGEventMaskBit(NX_SYSDEFINED),
|
||||||
|
tapEventCallback,
|
||||||
|
(__bridge void * __nullable)(self));
|
||||||
|
|
||||||
|
// Can be NULL if the app has no accessibility access permission
|
||||||
|
if (_eventPort == NULL)
|
||||||
|
return NO;
|
||||||
|
|
||||||
|
_eventPortSource = CFMachPortCreateRunLoopSource(kCFAllocatorSystemDefault, _eventPort, 0);
|
||||||
|
assert(_eventPortSource != NULL);
|
||||||
|
|
||||||
|
if (_eventPortSource == NULL)
|
||||||
|
return NO;
|
||||||
|
|
||||||
|
// Let's do this in a separate thread so that a slow app doesn't lag the event tap
|
||||||
|
_tapThread = [[NSThread alloc] initWithTarget:self
|
||||||
|
selector:@selector(eventTapThread)
|
||||||
|
object:nil];
|
||||||
|
[_tapThread start];
|
||||||
|
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)stopWatchingMediaKeys
|
||||||
|
{
|
||||||
|
// Shut down tap thread
|
||||||
|
if(_tapThreadRL){
|
||||||
|
CFRunLoopStop(_tapThreadRL);
|
||||||
|
_tapThreadRL = nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove tap port
|
||||||
|
if(_eventPort){
|
||||||
|
CFMachPortInvalidate(_eventPort);
|
||||||
|
CFRelease(_eventPort);
|
||||||
|
_eventPort = nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove tap source
|
||||||
|
if(_eventPortSource){
|
||||||
|
CFRelease(_eventPortSource);
|
||||||
|
_eventPortSource = nil;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark -
|
||||||
|
#pragma mark Accessors
|
||||||
|
|
||||||
|
+ (BOOL)usesGlobalMediaKeyTap
|
||||||
|
{
|
||||||
|
#ifdef _DEBUG
|
||||||
|
// breaking in gdb with a key tap inserted sometimes locks up all mouse and keyboard input forever, forcing reboot
|
||||||
|
return NO;
|
||||||
|
#else
|
||||||
|
// XXX(nevyn): MediaKey event tap doesn't work on 10.4, feel free to figure out why if you have the energy.
|
||||||
|
return
|
||||||
|
![[NSUserDefaults standardUserDefaults] boolForKey:kIgnoreMediaKeysDefaultsKey]
|
||||||
|
&& floor(NSAppKitVersionNumber) >= 949/*NSAppKitVersionNumber10_5*/;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
+ (NSArray*)mediaKeyUserBundleIdentifiers
|
||||||
|
{
|
||||||
|
return [NSArray arrayWithObjects:
|
||||||
|
[[NSBundle mainBundle] bundleIdentifier], // your app
|
||||||
|
@"com.spotify.client",
|
||||||
|
@"com.apple.iTunes",
|
||||||
|
@"com.apple.QuickTimePlayerX",
|
||||||
|
@"com.apple.quicktimeplayer",
|
||||||
|
@"com.apple.iWork.Keynote",
|
||||||
|
@"com.apple.iPhoto",
|
||||||
|
@"org.videolan.vlc",
|
||||||
|
@"com.apple.Aperture",
|
||||||
|
@"com.plexsquared.Plex",
|
||||||
|
@"com.soundcloud.desktop",
|
||||||
|
@"org.niltsh.MPlayerX",
|
||||||
|
@"com.ilabs.PandorasHelper",
|
||||||
|
@"com.mahasoftware.pandabar",
|
||||||
|
@"com.bitcartel.pandorajam",
|
||||||
|
@"org.clementine-player.clementine",
|
||||||
|
@"fm.last.Last.fm",
|
||||||
|
@"fm.last.Scrobbler",
|
||||||
|
@"com.beatport.BeatportPro",
|
||||||
|
@"com.Timenut.SongKey",
|
||||||
|
@"com.macromedia.fireworks", // the tap messes up their mouse input
|
||||||
|
@"at.justp.Theremin",
|
||||||
|
@"ru.ya.themblsha.YandexMusic",
|
||||||
|
@"com.jriver.MediaCenter18",
|
||||||
|
@"com.jriver.MediaCenter19",
|
||||||
|
@"com.jriver.MediaCenter20",
|
||||||
|
@"co.rackit.mate",
|
||||||
|
@"com.ttitt.b-music",
|
||||||
|
@"com.beardedspice.BeardedSpice",
|
||||||
|
@"com.plug.Plug",
|
||||||
|
@"com.plug.Plug2",
|
||||||
|
@"com.netease.163music",
|
||||||
|
@"org.quodlibet.quodlibet",
|
||||||
|
nil
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
- (BOOL)shouldInterceptMediaKeyEvents
|
||||||
|
{
|
||||||
|
BOOL shouldIntercept = NO;
|
||||||
|
@synchronized(self) {
|
||||||
|
shouldIntercept = _shouldInterceptMediaKeyEvents;
|
||||||
|
}
|
||||||
|
return shouldIntercept;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)pauseTapOnTapThread:(NSNumber *)yeahno
|
||||||
|
{
|
||||||
|
CGEventTapEnable(self->_eventPort, [yeahno boolValue]);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)setShouldInterceptMediaKeyEvents:(BOOL)newSetting
|
||||||
|
{
|
||||||
|
BOOL oldSetting;
|
||||||
|
@synchronized(self) {
|
||||||
|
oldSetting = _shouldInterceptMediaKeyEvents;
|
||||||
|
_shouldInterceptMediaKeyEvents = newSetting;
|
||||||
|
}
|
||||||
|
if(_tapThreadRL && oldSetting != newSetting) {
|
||||||
|
[self performSelector:@selector(pauseTapOnTapThread:)
|
||||||
|
onThread:_tapThread
|
||||||
|
withObject:@(newSetting)
|
||||||
|
waitUntilDone:NO];
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#pragma mark -
|
||||||
|
#pragma mark Event tap callbacks
|
||||||
|
|
||||||
|
// Note: method called on background thread
|
||||||
|
|
||||||
|
static CGEventRef tapEventCallback2(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon)
|
||||||
|
{
|
||||||
|
#pragma unused(proxy)
|
||||||
|
SPMediaKeyTap *self = (__bridge SPMediaKeyTap *)refcon;
|
||||||
|
|
||||||
|
if(type == kCGEventTapDisabledByTimeout) {
|
||||||
|
NSLog(@"Media key event tap was disabled by timeout");
|
||||||
|
CGEventTapEnable(self->_eventPort, TRUE);
|
||||||
|
return event;
|
||||||
|
} else if(type == kCGEventTapDisabledByUserInput) {
|
||||||
|
// Was disabled manually by -[pauseTapOnTapThread]
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
NSEvent *nsEvent = nil;
|
||||||
|
@try {
|
||||||
|
nsEvent = [NSEvent eventWithCGEvent:event];
|
||||||
|
}
|
||||||
|
@catch (NSException * e) {
|
||||||
|
NSLog(@"Strange CGEventType: %d: %@", type, e);
|
||||||
|
assert(0);
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type != NX_SYSDEFINED || [nsEvent subtype] != SPSystemDefinedEventMediaKeys)
|
||||||
|
return event;
|
||||||
|
|
||||||
|
int keyCode = (([nsEvent data1] & 0xFFFF0000) >> 16);
|
||||||
|
if (keyCode != NX_KEYTYPE_PLAY && keyCode != NX_KEYTYPE_FAST && keyCode != NX_KEYTYPE_REWIND && keyCode != NX_KEYTYPE_PREVIOUS && keyCode != NX_KEYTYPE_NEXT)
|
||||||
|
return event;
|
||||||
|
|
||||||
|
if (![self shouldInterceptMediaKeyEvents])
|
||||||
|
return event;
|
||||||
|
|
||||||
|
[self performSelectorOnMainThread:@selector(handleAndReleaseMediaKeyEvent:) withObject:nsEvent waitUntilDone:NO];
|
||||||
|
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static CGEventRef tapEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon)
|
||||||
|
{
|
||||||
|
@autoreleasepool {
|
||||||
|
CGEventRef ret = tapEventCallback2(proxy, type, event, refcon);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)handleAndReleaseMediaKeyEvent:(NSEvent *)event
|
||||||
|
{
|
||||||
|
[_delegate mediaKeyTap:self receivedMediaKeyEvent:event];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)eventTapThread
|
||||||
|
{
|
||||||
|
_tapThreadRL = CFRunLoopGetCurrent();
|
||||||
|
CFRunLoopAddSource(_tapThreadRL, _eventPortSource, kCFRunLoopCommonModes);
|
||||||
|
CFRunLoopRun();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#pragma mark -
|
||||||
|
#pragma mark Task switching callbacks
|
||||||
|
|
||||||
|
- (void)mediaKeyAppListChanged
|
||||||
|
{
|
||||||
|
#ifdef DEBUG_SPMEDIAKEY_APPLIST
|
||||||
|
[self debugPrintAppList];
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if([_mediaKeyAppList count] == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
NSRunningApplication *thisApp = [NSRunningApplication currentApplication];
|
||||||
|
NSRunningApplication *otherApp = [_mediaKeyAppList firstObject];
|
||||||
|
|
||||||
|
BOOL isCurrent = [thisApp isEqual:otherApp];
|
||||||
|
|
||||||
|
[self setShouldInterceptMediaKeyEvents:isCurrent];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)frontmostAppChanged:(NSNotification *)notification
|
||||||
|
{
|
||||||
|
NSRunningApplication *app = [notification.userInfo objectForKey:NSWorkspaceApplicationKey];
|
||||||
|
if (app.bundleIdentifier == nil)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (![[SPMediaKeyTap mediaKeyUserBundleIdentifiers] containsObject:app.bundleIdentifier])
|
||||||
|
return;
|
||||||
|
|
||||||
|
[_mediaKeyAppList removeObject:app];
|
||||||
|
[_mediaKeyAppList insertObject:app atIndex:0];
|
||||||
|
[self mediaKeyAppListChanged];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)appTerminated:(NSNotification *)notification
|
||||||
|
{
|
||||||
|
NSRunningApplication *app = [notification.userInfo objectForKey:NSWorkspaceApplicationKey];
|
||||||
|
[_mediaKeyAppList removeObject:app];
|
||||||
|
|
||||||
|
[self mediaKeyAppListChanged];
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef DEBUG_SPMEDIAKEY_APPLIST
|
||||||
|
- (void)debugPrintAppList
|
||||||
|
{
|
||||||
|
NSMutableString *list = [NSMutableString stringWithCapacity:255];
|
||||||
|
for (NSRunningApplication *app in _mediaKeyAppList) {
|
||||||
|
[list appendFormat:@" - %@\n", app.bundleIdentifier];
|
||||||
|
}
|
||||||
|
NSLog(@"List: \n%@", list);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
@end
|
||||||
52
3rdparty/discord-rpc/CMakeLists.txt
vendored
52
3rdparty/discord-rpc/CMakeLists.txt
vendored
@@ -1,52 +0,0 @@
|
|||||||
set(DISCORD_RPC_SOURCES
|
|
||||||
discord_rpc.h
|
|
||||||
discord_register.h
|
|
||||||
discord_rpc.cpp
|
|
||||||
discord_rpc_connection.h
|
|
||||||
discord_rpc_connection.cpp
|
|
||||||
discord_serialization.h
|
|
||||||
discord_serialization.cpp
|
|
||||||
discord_connection.h
|
|
||||||
discord_backoff.h
|
|
||||||
discord_msg_queue.h
|
|
||||||
)
|
|
||||||
|
|
||||||
if(UNIX)
|
|
||||||
list(APPEND DISCORD_RPC_SOURCES discord_connection_unix.cpp)
|
|
||||||
if(APPLE)
|
|
||||||
list(APPEND DISCORD_RPC_SOURCES discord_register_osx.m)
|
|
||||||
add_definitions(-DDISCORD_OSX)
|
|
||||||
else()
|
|
||||||
list(APPEND DISCORD_RPC_SOURCES discord_register_linux.cpp)
|
|
||||||
add_definitions(-DDISCORD_LINUX)
|
|
||||||
endif()
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(WIN32)
|
|
||||||
list(APPEND DISCORD_RPC_SOURCES discord_connection_win.cpp discord_register_win.cpp)
|
|
||||||
add_definitions(-DDISCORD_WINDOWS)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
add_library(discord-rpc STATIC ${DISCORD_RPC_SOURCES})
|
|
||||||
|
|
||||||
if(APPLE)
|
|
||||||
target_link_libraries(discord-rpc PRIVATE "-framework AppKit")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# RapidJSON (as packaged by Homebrew and others) can trigger C++17 deprecation
|
|
||||||
# warnings (e.g. std::iterator) when compiled with AppleClang/libc++.
|
|
||||||
# Keep the suppression narrowly scoped to this 3rdparty target.
|
|
||||||
if(APPLE AND CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
|
||||||
target_compile_options(discord-rpc PRIVATE -Wno-deprecated-declarations)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(WIN32)
|
|
||||||
target_link_libraries(discord-rpc PRIVATE psapi advapi32)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(TARGET RapidJSON::RapidJSON)
|
|
||||||
target_link_libraries(discord-rpc PRIVATE RapidJSON::RapidJSON)
|
|
||||||
elseif(RapidJSON_INCLUDE_DIRS)
|
|
||||||
target_include_directories(discord-rpc SYSTEM PRIVATE ${RapidJSON_INCLUDE_DIRS})
|
|
||||||
endif()
|
|
||||||
target_include_directories(discord-rpc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
|
||||||
19
3rdparty/discord-rpc/LICENSE
vendored
19
3rdparty/discord-rpc/LICENSE
vendored
@@ -1,19 +0,0 @@
|
|||||||
Copyright 2017 Discord, Inc.
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
this software and associated documentation files (the "Software"), to deal in
|
|
||||||
the Software without restriction, including without limitation the rights to
|
|
||||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
||||||
of the Software, and to permit persons to whom the Software is furnished to do
|
|
||||||
so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
162
3rdparty/discord-rpc/README.md
vendored
162
3rdparty/discord-rpc/README.md
vendored
@@ -1,162 +0,0 @@
|
|||||||
# Discord RPC
|
|
||||||
|
|
||||||
## Fork Notice
|
|
||||||
|
|
||||||
This library was slightly modified for Strawberry Music Player with some extra features from the new API and shared library support/more unnecessary components removed. The original repository is [here](https://github.com/discord/discord-rpc)
|
|
||||||
|
|
||||||
## Deprecation Notice
|
|
||||||
|
|
||||||
This library has been deprecated in favor of Discord's GameSDK. [Learn more here](https://discordapp.com/developers/docs/game-sdk/sdk-starter-guide)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
This is a library for interfacing your game with a locally running Discord desktop client. It's known to work on Windows, macOS, and Linux. You can use the lib directly if you like, or use it as a guide to writing your own if it doesn't suit your game as is. PRs/feedback welcome if you have an improvement everyone might want, or can describe how this doesn't meet your needs.
|
|
||||||
|
|
||||||
Included here are some quick demos that implement the very minimal subset to show current status, and
|
|
||||||
have callbacks for where a more complete game would do more things (joining, spectating, etc).
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
The most up to date documentation for Rich Presence can always be found on our [developer site](https://discordapp.com/developers/docs/rich-presence/how-to)! If you're interested in rolling your own native implementation of Rich Presence via IPC sockets instead of using our SDK—hey, you've got free time, right?—check out the ["Hard Mode" documentation](https://github.com/discordapp/discord-rpc/blob/master/documentation/hard-mode.md).
|
|
||||||
|
|
||||||
## Basic Usage
|
|
||||||
|
|
||||||
Zeroith, you should be set up to build things because you are a game developer, right?
|
|
||||||
|
|
||||||
First, head on over to the [Discord developers site](https://discordapp.com/developers/applications/me) and make yourself an app. Keep track of `Client ID` -- you'll need it here to pass to the init function.
|
|
||||||
|
|
||||||
### Unreal Engine 4 Setup
|
|
||||||
|
|
||||||
To use the Rich Presense plugin with Unreal Engine Projects:
|
|
||||||
|
|
||||||
1. Download the latest [release](https://github.com/discordapp/discord-rpc/releases) for each operating system you are targeting and the zipped source code
|
|
||||||
2. In the source code zip, copy the UE plugin—`examples/unrealstatus/Plugins/discordrpc`—to your project's plugin directory
|
|
||||||
3. At `[YOUR_UE_PROJECT]/Plugins/discordrpc/source/ThirdParty/DiscordRpcLibrary/`, create an `Include` folder and copy `discord_rpc.h` and `discord_register.h` to it from the zip
|
|
||||||
4. Follow the steps below for each OS
|
|
||||||
5. Build your UE4 project
|
|
||||||
6. Launch the editor, and enable the Discord plugin.
|
|
||||||
|
|
||||||
#### Windows
|
|
||||||
|
|
||||||
- At `[YOUR_UE_PROJECT]/Plugins/discordrpc/source/ThirdParty/DiscordRpcLibrary/`, create a `Win64` folder
|
|
||||||
- Copy `lib/discord-rpc.lib` and `bin/discord-rpc.dll` from `[RELEASE_ZIP]/win64-dynamic` to the `Win64` folder
|
|
||||||
|
|
||||||
#### Mac
|
|
||||||
|
|
||||||
- At `[YOUR_UE_PROJECT]/Plugins/discordrpc/source/ThirdParty/DiscordRpcLibrary/`, create a `Mac` folder
|
|
||||||
- Copy `libdiscord-rpc.dylib` from `[RELEASE_ZIP]/osx-dynamic/lib` to the `Mac` folder
|
|
||||||
|
|
||||||
#### Linux
|
|
||||||
|
|
||||||
- At `[YOUR_UE_PROJECT]/Plugins/discordrpc/source/ThirdParty/DiscordRpcLibrary/`, create a `Linux` folder
|
|
||||||
- Inside, create another folder `x86_64-unknown-linux-gnu`
|
|
||||||
- Copy `libdiscord-rpc.so` from `[RELEASE_ZIP]/linux-dynamic/lib` to `Linux/x86_64-unknown-linux-gnu`
|
|
||||||
|
|
||||||
### Unity Setup
|
|
||||||
|
|
||||||
If you're a Unity developer looking to integrate Rich Presence into your game, follow this simple guide to get started towards success:
|
|
||||||
|
|
||||||
1. Download the DLLs for any platform that you need from [our releases](https://github.com/discordapp/discord-rpc/releases)
|
|
||||||
2. In your Unity project, create a `Plugins` folder inside your `Assets` folder if you don't already have one
|
|
||||||
3. Copy the file `DiscordRpc.cs` from [here](https://github.com/discordapp/discord-rpc/blob/master/examples/button-clicker/Assets/DiscordRpc.cs) into your `Assets` folder. This is basically your header file for the SDK
|
|
||||||
|
|
||||||
We've got our `Plugins` folder ready, so let's get platform-specific!
|
|
||||||
|
|
||||||
#### Windows
|
|
||||||
|
|
||||||
4. Create `x86` and `x86_64` folders inside `Assets/Plugins/`
|
|
||||||
5. Copy `discord-rpc-win/win64-dynamic/bin/discord-rpc.dll` to `Assets/Plugins/x86_64/`
|
|
||||||
6. Copy `discord-rpc-win/win32-dynamic/bin/discord-rpc.dll` to `Assets/Plugins/x86/`
|
|
||||||
7. Click on both DLLs and make sure they are targetting the correct architectures in the Unity editor properties pane
|
|
||||||
8. Done!
|
|
||||||
|
|
||||||
#### MacOS
|
|
||||||
|
|
||||||
4. Copy `discord-rpc-osx/osx-dynamic/lib/libdiscord-rpc.dylib` to `Assets/Plugins/`
|
|
||||||
5. Rename `libdiscord-rpc.dylib` to `discord-rpc.bundle`
|
|
||||||
6. Done!
|
|
||||||
|
|
||||||
#### Linux
|
|
||||||
|
|
||||||
4. Copy `discord-rpc-linux/linux-dynamic-lib/libdiscord-rpc.so` to `Assets/Plugins/`
|
|
||||||
5. Done!
|
|
||||||
|
|
||||||
You're ready to roll! For code examples on how to interact with the SDK using the `DiscordRpc.cs` header file, check out [our example](https://github.com/discordapp/discord-rpc/blob/master/examples/button-clicker/Assets/DiscordController.cs)
|
|
||||||
|
|
||||||
### From package
|
|
||||||
|
|
||||||
Download a release package for your platform(s) -- they have subdirs with various prebuilt options, select the one you need add `/include` to your compile includes, `/lib` to your linker paths, and link with `discord-rpc`. For the dynamically linked builds, you'll need to ship the associated file along with your game.
|
|
||||||
|
|
||||||
### From repo
|
|
||||||
|
|
||||||
First-eth, you'll want `CMake`. There's a few different ways to install it on your system, and you should refer to [their website](https://cmake.org/install/). Many package managers provide ways of installing CMake as well.
|
|
||||||
|
|
||||||
To make sure it's installed correctly, type `cmake --version` into your flavor of terminal/cmd. If you get a response with a version number, you're good to go!
|
|
||||||
|
|
||||||
There's a [CMake](https://cmake.org/download/) file that should be able to generate the lib for you; Sometimes I use it like this:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cd <path to discord-rpc>
|
|
||||||
mkdir build
|
|
||||||
cd build
|
|
||||||
cmake .. -DCMAKE_INSTALL_PREFIX=<path to install discord-rpc to>
|
|
||||||
cmake --build . --config Release --target install
|
|
||||||
```
|
|
||||||
|
|
||||||
There is a wrapper build script `build.py` that runs `cmake` with a few different options.
|
|
||||||
|
|
||||||
Usually, I run `build.py` to get things started, then use the generated project files as I work on things. It does depend on `click` library, so do a quick `pip install click` to make sure you have it if you want to run `build.py`.
|
|
||||||
|
|
||||||
There are some CMake options you might care about:
|
|
||||||
|
|
||||||
| flag | default | does |
|
|
||||||
| ---------------------------------------------------------------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
||||||
| `ENABLE_IO_THREAD` | `ON` | When enabled, we start up a thread to do io processing, if disabled you should call `Discord_UpdateConnection` yourself. |
|
|
||||||
| `USE_STATIC_CRT` | `OFF` | (Windows) Enable to statically link the CRT, avoiding requiring users install the redistributable package. (The prebuilt binaries enable this option) |
|
|
||||||
| [`BUILD_SHARED_LIBS`](https://cmake.org/cmake/help/v3.7/variable/BUILD_SHARED_LIBS.html) | `OFF` | Build library as a DLL |
|
|
||||||
| `WARNINGS_AS_ERRORS` | `OFF` | When enabled, compiles with `-Werror` (on \*nix platforms). |
|
|
||||||
|
|
||||||
## Continuous Builds
|
|
||||||
|
|
||||||
Why do we have three of these? Three times the fun!
|
|
||||||
|
|
||||||
| CI | badge |
|
|
||||||
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
||||||
| TravisCI | [](https://travis-ci.org/discordapp/discord-rpc) |
|
|
||||||
| AppVeyor | [](https://ci.appveyor.com/project/crmarsh/discord-rpc) |
|
|
||||||
| Buildkite (internal) | [](https://buildkite.com/discord/discord-rpc) |
|
|
||||||
|
|
||||||
## Sample: send-presence
|
|
||||||
|
|
||||||
This is a text adventure "game" that inits/deinits the connection to Discord, and sends a presence update on each command.
|
|
||||||
|
|
||||||
## Sample: button-clicker
|
|
||||||
|
|
||||||
This is a sample [Unity](https://unity3d.com/) project that wraps a DLL version of the library, and sends presence updates when you click on a button. Run `python build.py unity` in the root directory to build the correct library files and place them in their respective folders.
|
|
||||||
|
|
||||||
## Sample: unrealstatus
|
|
||||||
|
|
||||||
This is a sample [Unreal](https://www.unrealengine.com) project that wraps the DLL version of the library with an Unreal plugin, exposes a blueprint class for interacting with it, and uses that to make a very simple UI. Run `python build.py unreal` in the root directory to build the correct library files and place them in their respective folders.
|
|
||||||
|
|
||||||
## Wrappers and Implementations
|
|
||||||
|
|
||||||
Below is a table of unofficial, community-developed wrappers for and implementations of Rich Presence in various languages. If you would like to have yours added, please make a pull request adding your repository to the table. The repository should include:
|
|
||||||
|
|
||||||
- The code
|
|
||||||
- A brief ReadMe of how to use it
|
|
||||||
- A working example
|
|
||||||
|
|
||||||
###### Rich Presence Wrappers and Implementations
|
|
||||||
|
|
||||||
| Name | Language |
|
|
||||||
| ------------------------------------------------------------------------- | --------------------------------- |
|
|
||||||
| [Discord RPC C#](https://github.com/Lachee/discord-rpc-csharp) | C# |
|
|
||||||
| [Discord RPC D](https://github.com/voidblaster/discord-rpc-d) | [D](https://dlang.org/) |
|
|
||||||
| [discord-rpc.jar](https://github.com/Vatuu/discord-rpc 'Discord-RPC.jar') | Java |
|
|
||||||
| [java-discord-rpc](https://github.com/MinnDevelopment/java-discord-rpc) | Java |
|
|
||||||
| [Discord-IPC](https://github.com/jagrosh/DiscordIPC) | Java |
|
|
||||||
| [Discord Rich Presence](https://npmjs.org/discord-rich-presence) | JavaScript |
|
|
||||||
| [drpc4k](https://github.com/Bluexin/drpc4k) | [Kotlin](https://kotlinlang.org/) |
|
|
||||||
| [lua-discordRPC](https://github.com/pfirsich/lua-discordRPC) | LuaJIT (FFI) |
|
|
||||||
| [pypresence](https://github.com/qwertyquerty/pypresence) | [Python](https://python.org/) |
|
|
||||||
| [SwordRPC](https://github.com/Azoy/SwordRPC) | [Swift](https://swift.org) |
|
|
||||||
63
3rdparty/discord-rpc/discord_backoff.h
vendored
63
3rdparty/discord-rpc/discord_backoff.h
vendored
@@ -1,63 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2017 Discord, Inc.
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
* this software and associated documentation files (the "Software"), to deal in
|
|
||||||
* the Software without restriction, including without limitation the rights to
|
|
||||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
||||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
|
||||||
* so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef DISCORD_BACKOFF_H
|
|
||||||
#define DISCORD_BACKOFF_H
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <random>
|
|
||||||
#include <cstdint>
|
|
||||||
#include <ctime>
|
|
||||||
|
|
||||||
namespace discord_rpc {
|
|
||||||
|
|
||||||
struct Backoff {
|
|
||||||
int64_t minAmount;
|
|
||||||
int64_t maxAmount;
|
|
||||||
int64_t current;
|
|
||||||
int fails;
|
|
||||||
std::mt19937_64 randGenerator;
|
|
||||||
std::uniform_real_distribution<> randDistribution;
|
|
||||||
|
|
||||||
double rand01() { return randDistribution(randGenerator); }
|
|
||||||
|
|
||||||
Backoff(int64_t min, int64_t max)
|
|
||||||
: minAmount(min), maxAmount(max), current(min), fails(0), randGenerator(static_cast<uint64_t>(time(0))) {
|
|
||||||
}
|
|
||||||
|
|
||||||
void reset() {
|
|
||||||
fails = 0;
|
|
||||||
current = minAmount;
|
|
||||||
}
|
|
||||||
|
|
||||||
int64_t nextDelay() {
|
|
||||||
++fails;
|
|
||||||
int64_t delay = static_cast<int64_t>(static_cast<double>(current) * 2.0 * rand01());
|
|
||||||
current = std::min(current + delay, maxAmount);
|
|
||||||
return current;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace discord_rpc
|
|
||||||
|
|
||||||
#endif // DISCORD_BACKOFF_H
|
|
||||||
48
3rdparty/discord-rpc/discord_connection.h
vendored
48
3rdparty/discord-rpc/discord_connection.h
vendored
@@ -1,48 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2017 Discord, Inc.
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
* this software and associated documentation files (the "Software"), to deal in
|
|
||||||
* the Software without restriction, including without limitation the rights to
|
|
||||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
||||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
|
||||||
* so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef DISCORD_CONNECTION_H
|
|
||||||
#define DISCORD_CONNECTION_H
|
|
||||||
|
|
||||||
// This is to wrap the platform specific kinds of connect/read/write.
|
|
||||||
|
|
||||||
#include <cstdlib>
|
|
||||||
|
|
||||||
namespace discord_rpc {
|
|
||||||
|
|
||||||
// not really connectiony, but need per-platform
|
|
||||||
int GetProcessId();
|
|
||||||
|
|
||||||
struct BaseConnection {
|
|
||||||
static BaseConnection *Create();
|
|
||||||
static void Destroy(BaseConnection*&);
|
|
||||||
bool isOpen = false;
|
|
||||||
bool Open();
|
|
||||||
bool Close();
|
|
||||||
bool Write(const void *data, size_t length);
|
|
||||||
bool Read(void *data, size_t length);
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace discord_rpc
|
|
||||||
|
|
||||||
#endif // DISCORD_CONNECTION_H
|
|
||||||
160
3rdparty/discord-rpc/discord_connection_unix.cpp
vendored
160
3rdparty/discord-rpc/discord_connection_unix.cpp
vendored
@@ -1,160 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2017 Discord, Inc.
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
* this software and associated documentation files (the "Software"), to deal in
|
|
||||||
* the Software without restriction, including without limitation the rights to
|
|
||||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
||||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
|
||||||
* so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "discord_connection.h"
|
|
||||||
|
|
||||||
#include <cerrno>
|
|
||||||
#include <fcntl.h>
|
|
||||||
#include <cstdio>
|
|
||||||
#include <cstring>
|
|
||||||
#include <sys/socket.h>
|
|
||||||
#include <sys/types.h>
|
|
||||||
#include <sys/un.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
|
|
||||||
namespace discord_rpc {
|
|
||||||
|
|
||||||
int GetProcessId() {
|
|
||||||
return ::getpid();
|
|
||||||
}
|
|
||||||
|
|
||||||
struct BaseConnectionUnix : public BaseConnection {
|
|
||||||
int sock{ -1 };
|
|
||||||
};
|
|
||||||
|
|
||||||
static BaseConnectionUnix Connection;
|
|
||||||
static sockaddr_un PipeAddr{};
|
|
||||||
#ifdef MSG_NOSIGNAL
|
|
||||||
static int MsgFlags = MSG_NOSIGNAL;
|
|
||||||
#else
|
|
||||||
static int MsgFlags = 0;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
static const char *GetTempPath() {
|
|
||||||
|
|
||||||
const char *temp = getenv("XDG_RUNTIME_DIR");
|
|
||||||
temp = temp ? temp : getenv("TMPDIR");
|
|
||||||
temp = temp ? temp : getenv("TMP");
|
|
||||||
temp = temp ? temp : getenv("TEMP");
|
|
||||||
temp = temp ? temp : "/tmp";
|
|
||||||
|
|
||||||
return temp;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
BaseConnection *BaseConnection::Create() {
|
|
||||||
PipeAddr.sun_family = AF_UNIX;
|
|
||||||
return &Connection;
|
|
||||||
}
|
|
||||||
|
|
||||||
void BaseConnection::Destroy(BaseConnection *&c) {
|
|
||||||
|
|
||||||
auto self = reinterpret_cast<BaseConnectionUnix*>(c);
|
|
||||||
self->Close();
|
|
||||||
c = nullptr;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
bool BaseConnection::Open() {
|
|
||||||
|
|
||||||
const char *tempPath = GetTempPath();
|
|
||||||
auto self = reinterpret_cast<BaseConnectionUnix*>(this);
|
|
||||||
self->sock = socket(AF_UNIX, SOCK_STREAM, 0);
|
|
||||||
if (self->sock == -1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
fcntl(self->sock, F_SETFL, O_NONBLOCK);
|
|
||||||
#ifdef SO_NOSIGPIPE
|
|
||||||
int optval = 1;
|
|
||||||
setsockopt(self->sock, SOL_SOCKET, SO_NOSIGPIPE, &optval, sizeof(optval));
|
|
||||||
#endif
|
|
||||||
|
|
||||||
for (int pipeNum = 0; pipeNum < 10; ++pipeNum) {
|
|
||||||
snprintf(PipeAddr.sun_path, sizeof(PipeAddr.sun_path), "%s/discord-ipc-%d", tempPath, pipeNum);
|
|
||||||
int err = connect(self->sock, reinterpret_cast<const sockaddr*>(&PipeAddr), sizeof(PipeAddr));
|
|
||||||
if (err == 0) {
|
|
||||||
self->isOpen = true;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self->Close();
|
|
||||||
|
|
||||||
return false;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
bool BaseConnection::Close() {
|
|
||||||
|
|
||||||
auto self = reinterpret_cast<BaseConnectionUnix*>(this);
|
|
||||||
if (self->sock == -1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
close(self->sock);
|
|
||||||
self->sock = -1;
|
|
||||||
self->isOpen = false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
bool BaseConnection::Write(const void *data, size_t length) {
|
|
||||||
|
|
||||||
auto self = reinterpret_cast<BaseConnectionUnix*>(this);
|
|
||||||
|
|
||||||
if (self->sock == -1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ssize_t sentBytes = send(self->sock, data, length, MsgFlags);
|
|
||||||
if (sentBytes < 0) {
|
|
||||||
Close();
|
|
||||||
}
|
|
||||||
|
|
||||||
return sentBytes == static_cast<ssize_t>(length);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
bool BaseConnection::Read(void *data, size_t length) {
|
|
||||||
|
|
||||||
auto self = reinterpret_cast<BaseConnectionUnix*>(this);
|
|
||||||
|
|
||||||
if (self->sock == -1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
long res = recv(self->sock, data, length, MsgFlags);
|
|
||||||
if (res < 0) {
|
|
||||||
if (errno == EAGAIN) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Close();
|
|
||||||
}
|
|
||||||
else if (res == 0) {
|
|
||||||
Close();
|
|
||||||
}
|
|
||||||
|
|
||||||
return static_cast<size_t>(res) == length;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace discord_rpc
|
|
||||||
160
3rdparty/discord-rpc/discord_connection_win.cpp
vendored
160
3rdparty/discord-rpc/discord_connection_win.cpp
vendored
@@ -1,160 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2017 Discord, Inc.
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
* this software and associated documentation files (the "Software"), to deal in
|
|
||||||
* the Software without restriction, including without limitation the rights to
|
|
||||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
||||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
|
||||||
* so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "discord_connection.h"
|
|
||||||
|
|
||||||
#define WIN32_LEAN_AND_MEAN
|
|
||||||
#define NOMCX
|
|
||||||
#define NOSERVICE
|
|
||||||
#define NOIME
|
|
||||||
|
|
||||||
#include <cassert>
|
|
||||||
#include <windows.h>
|
|
||||||
|
|
||||||
namespace discord_rpc {
|
|
||||||
|
|
||||||
int GetProcessId() {
|
|
||||||
return static_cast<int>(::GetCurrentProcessId());
|
|
||||||
}
|
|
||||||
|
|
||||||
struct BaseConnectionWin : public BaseConnection {
|
|
||||||
HANDLE pipe{ INVALID_HANDLE_VALUE };
|
|
||||||
};
|
|
||||||
|
|
||||||
static BaseConnectionWin Connection;
|
|
||||||
|
|
||||||
BaseConnection *BaseConnection::Create() {
|
|
||||||
return &Connection;
|
|
||||||
}
|
|
||||||
|
|
||||||
void BaseConnection::Destroy(BaseConnection *&c) {
|
|
||||||
|
|
||||||
auto self = reinterpret_cast<BaseConnectionWin*>(c);
|
|
||||||
self->Close();
|
|
||||||
c = nullptr;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
bool BaseConnection::Open() {
|
|
||||||
|
|
||||||
wchar_t pipeName[]{ L"\\\\?\\pipe\\discord-ipc-0" };
|
|
||||||
const size_t pipeDigit = sizeof(pipeName) / sizeof(wchar_t) - 2;
|
|
||||||
pipeName[pipeDigit] = L'0';
|
|
||||||
auto self = reinterpret_cast<BaseConnectionWin*>(this);
|
|
||||||
for (;;) {
|
|
||||||
self->pipe = ::CreateFileW(pipeName, GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);
|
|
||||||
if (self->pipe != INVALID_HANDLE_VALUE) {
|
|
||||||
self->isOpen = true;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto lastError = GetLastError();
|
|
||||||
if (lastError == ERROR_FILE_NOT_FOUND) {
|
|
||||||
if (pipeName[pipeDigit] < L'9') {
|
|
||||||
pipeName[pipeDigit]++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (lastError == ERROR_PIPE_BUSY) {
|
|
||||||
if (!WaitNamedPipeW(pipeName, 10000)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
bool BaseConnection::Close() {
|
|
||||||
|
|
||||||
auto self = reinterpret_cast<BaseConnectionWin*>(this);
|
|
||||||
::CloseHandle(self->pipe);
|
|
||||||
self->pipe = INVALID_HANDLE_VALUE;
|
|
||||||
self->isOpen = false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
bool BaseConnection::Write(const void *data, size_t length) {
|
|
||||||
|
|
||||||
if (length == 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
auto self = reinterpret_cast<BaseConnectionWin*>(this);
|
|
||||||
assert(self);
|
|
||||||
if (!self) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (self->pipe == INVALID_HANDLE_VALUE) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
assert(data);
|
|
||||||
if (!data) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const DWORD bytesLength = static_cast<DWORD>(length);
|
|
||||||
DWORD bytesWritten = 0;
|
|
||||||
|
|
||||||
return ::WriteFile(self->pipe, data, bytesLength, &bytesWritten, nullptr) == TRUE && bytesWritten == bytesLength;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
bool BaseConnection::Read(void *data, size_t length) {
|
|
||||||
|
|
||||||
assert(data);
|
|
||||||
if (!data) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
auto self = reinterpret_cast<BaseConnectionWin*>(this);
|
|
||||||
assert(self);
|
|
||||||
if (!self) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (self->pipe == INVALID_HANDLE_VALUE) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
DWORD bytesAvailable = 0;
|
|
||||||
if (::PeekNamedPipe(self->pipe, nullptr, 0, nullptr, &bytesAvailable, nullptr)) {
|
|
||||||
if (bytesAvailable >= length) {
|
|
||||||
DWORD bytesToRead = static_cast<DWORD>(length);
|
|
||||||
DWORD bytesRead = 0;
|
|
||||||
if (::ReadFile(self->pipe, data, bytesToRead, &bytesRead, nullptr) == TRUE) {
|
|
||||||
assert(bytesToRead == bytesRead);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Close();
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace discord_rpc
|
|
||||||
64
3rdparty/discord-rpc/discord_msg_queue.h
vendored
64
3rdparty/discord-rpc/discord_msg_queue.h
vendored
@@ -1,64 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2017 Discord, Inc.
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
* this software and associated documentation files (the "Software"), to deal in
|
|
||||||
* the Software without restriction, including without limitation the rights to
|
|
||||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
||||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
|
||||||
* so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef DISCORD_MSG_QUEUE_H
|
|
||||||
#define DISCORD_MSG_QUEUE_H
|
|
||||||
|
|
||||||
#include <atomic>
|
|
||||||
|
|
||||||
// A simple queue. No locks, but only works with a single thread as producer and a single thread as
|
|
||||||
// a consumer. Mutex up as needed.
|
|
||||||
|
|
||||||
namespace discord_rpc {
|
|
||||||
|
|
||||||
template<typename ElementType, std::size_t QueueSize>
|
|
||||||
class MsgQueue {
|
|
||||||
ElementType queue_[QueueSize];
|
|
||||||
std::atomic_uint nextAdd_{ 0 };
|
|
||||||
std::atomic_uint nextSend_{ 0 };
|
|
||||||
std::atomic_uint pendingSends_{ 0 };
|
|
||||||
|
|
||||||
public:
|
|
||||||
MsgQueue() {}
|
|
||||||
|
|
||||||
ElementType *GetNextAddMessage() {
|
|
||||||
// if we are falling behind, bail
|
|
||||||
if (pendingSends_.load() >= QueueSize) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
auto index = (nextAdd_++) % QueueSize;
|
|
||||||
return &queue_[index];
|
|
||||||
}
|
|
||||||
void CommitAdd() { ++pendingSends_; }
|
|
||||||
|
|
||||||
bool HavePendingSends() const { return pendingSends_.load() != 0; }
|
|
||||||
ElementType *GetNextSendMessage() {
|
|
||||||
auto index = (nextSend_++) % QueueSize;
|
|
||||||
return &queue_[index];
|
|
||||||
}
|
|
||||||
void CommitSend() { --pendingSends_; }
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace discord_rpc
|
|
||||||
|
|
||||||
#endif // DISCORD_MSG_QUEUE_H
|
|
||||||
37
3rdparty/discord-rpc/discord_register.h
vendored
37
3rdparty/discord-rpc/discord_register.h
vendored
@@ -1,37 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2017 Discord, Inc.
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
* this software and associated documentation files (the "Software"), to deal in
|
|
||||||
* the Software without restriction, including without limitation the rights to
|
|
||||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
||||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
|
||||||
* so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef DISCORD_REGISTER_H
|
|
||||||
#define DISCORD_REGISTER_H
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
extern "C" {
|
|
||||||
#endif
|
|
||||||
|
|
||||||
void Discord_Register(const char *applicationId, const char *command);
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#endif // DISCORD_REGISTER_H
|
|
||||||
120
3rdparty/discord-rpc/discord_register_linux.cpp
vendored
120
3rdparty/discord-rpc/discord_register_linux.cpp
vendored
@@ -1,120 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2017 Discord, Inc.
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
* this software and associated documentation files (the "Software"), to deal in
|
|
||||||
* the Software without restriction, including without limitation the rights to
|
|
||||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
||||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
|
||||||
* so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "discord_rpc.h"
|
|
||||||
#include "discord_register.h"
|
|
||||||
|
|
||||||
#include <cstdio>
|
|
||||||
#include <errno.h>
|
|
||||||
#include <cstdlib>
|
|
||||||
#include <cstring>
|
|
||||||
#include <sys/stat.h>
|
|
||||||
#include <sys/types.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
static bool Mkdir(const char *path) {
|
|
||||||
int result = mkdir(path, 0755);
|
|
||||||
if (result == 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (errno == EEXIST) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
// We want to register games so we can run them from Discord client as discord-<appid>://
|
|
||||||
extern "C" void Discord_Register(const char *applicationId, const char *command) {
|
|
||||||
|
|
||||||
// Add a desktop file and update some mime handlers so that xdg-open does the right thing.
|
|
||||||
|
|
||||||
const char *home = getenv("HOME");
|
|
||||||
if (!home) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
char exePath[1024]{};
|
|
||||||
if (!command || !command[0]) {
|
|
||||||
const ssize_t size = readlink("/proc/self/exe", exePath, sizeof(exePath));
|
|
||||||
if (size <= 0 || size >= static_cast<ssize_t>(sizeof(exePath))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
exePath[size] = '\0';
|
|
||||||
command = exePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
constexpr char desktopFileFormat[] = "[Desktop Entry]\n"
|
|
||||||
"Name=Game %s\n"
|
|
||||||
"Exec=%s %%u\n" // note: it really wants that %u in there
|
|
||||||
"Type=Application\n"
|
|
||||||
"NoDisplay=true\n"
|
|
||||||
"Categories=Discord;Games;\n"
|
|
||||||
"MimeType=x-scheme-handler/discord-%s;\n";
|
|
||||||
char desktopFile[2048]{};
|
|
||||||
int fileLen = snprintf(desktopFile, sizeof(desktopFile), desktopFileFormat, applicationId, command, applicationId);
|
|
||||||
if (fileLen <= 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
char desktopFilename[256]{};
|
|
||||||
(void)snprintf(desktopFilename, sizeof(desktopFilename), "/discord-%s.desktop", applicationId);
|
|
||||||
|
|
||||||
char desktopFilePath[1024]{};
|
|
||||||
(void)snprintf(desktopFilePath, sizeof(desktopFilePath), "%s/.local", home);
|
|
||||||
if (!Mkdir(desktopFilePath)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
strcat(desktopFilePath, "/share");
|
|
||||||
if (!Mkdir(desktopFilePath)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
strcat(desktopFilePath, "/applications");
|
|
||||||
if (!Mkdir(desktopFilePath)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
strcat(desktopFilePath, desktopFilename);
|
|
||||||
|
|
||||||
FILE *fp = fopen(desktopFilePath, "w");
|
|
||||||
if (fp) {
|
|
||||||
fwrite(desktopFile, 1, fileLen, fp);
|
|
||||||
fclose(fp);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
char xdgMimeCommand[1024]{};
|
|
||||||
snprintf(xdgMimeCommand,
|
|
||||||
sizeof(xdgMimeCommand),
|
|
||||||
"xdg-mime default discord-%s.desktop x-scheme-handler/discord-%s",
|
|
||||||
applicationId,
|
|
||||||
applicationId);
|
|
||||||
if (system(xdgMimeCommand) < 0) {
|
|
||||||
fprintf(stderr, "Failed to register mime handler\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
99
3rdparty/discord-rpc/discord_register_osx.m
vendored
99
3rdparty/discord-rpc/discord_register_osx.m
vendored
@@ -1,99 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2017 Discord, Inc.
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
* this software and associated documentation files (the "Software"), to deal in
|
|
||||||
* the Software without restriction, including without limitation the rights to
|
|
||||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
||||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
|
||||||
* so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <sys/stat.h>
|
|
||||||
|
|
||||||
#import <AppKit/AppKit.h>
|
|
||||||
|
|
||||||
#include "discord_register.h"
|
|
||||||
|
|
||||||
static void RegisterCommand(const char *applicationId, const char *command) {
|
|
||||||
|
|
||||||
// There does not appear to be a way to register arbitrary commands on OSX, so instead we'll save the command
|
|
||||||
// to a file in the Discord config path, and when it is needed, Discord can try to load the file there, open
|
|
||||||
// the command therein (will pass to js's window.open, so requires a url-like thing)
|
|
||||||
|
|
||||||
// Note: will not work for sandboxed apps
|
|
||||||
NSString *home = NSHomeDirectory();
|
|
||||||
if (!home) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSString *path = [[[[[[home stringByAppendingPathComponent:@"Library"]
|
|
||||||
stringByAppendingPathComponent:@"Application Support"]
|
|
||||||
stringByAppendingPathComponent:@"discord"]
|
|
||||||
stringByAppendingPathComponent:@"games"]
|
|
||||||
stringByAppendingPathComponent:[NSString stringWithUTF8String:applicationId]]
|
|
||||||
stringByAppendingPathExtension:@"json"];
|
|
||||||
[[NSFileManager defaultManager] createDirectoryAtPath:[path stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:nil];
|
|
||||||
|
|
||||||
NSString *jsonBuffer = [NSString stringWithFormat:@"{\"command\": \"%s\"}", command];
|
|
||||||
[jsonBuffer writeToFile:path atomically:NO encoding:NSUTF8StringEncoding error:nil];
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
static void RegisterURL(const char *applicationId) {
|
|
||||||
|
|
||||||
char url[256];
|
|
||||||
snprintf(url, sizeof(url), "discord-%s", applicationId);
|
|
||||||
CFStringRef cfURL = CFStringCreateWithCString(NULL, url, kCFStringEncodingUTF8);
|
|
||||||
|
|
||||||
NSString* myBundleId = [[NSBundle mainBundle] bundleIdentifier];
|
|
||||||
if (!myBundleId) {
|
|
||||||
fprintf(stderr, "No bundle id found\n");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSURL* myURL = [[NSBundle mainBundle] bundleURL];
|
|
||||||
if (!myURL) {
|
|
||||||
fprintf(stderr, "No bundle url found\n");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
OSStatus status = LSSetDefaultHandlerForURLScheme(cfURL, (__bridge CFStringRef)myBundleId);
|
|
||||||
if (status != noErr) {
|
|
||||||
fprintf(stderr, "Error in LSSetDefaultHandlerForURLScheme: %d\n", (int)status);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
status = LSRegisterURL((__bridge CFURLRef)myURL, true);
|
|
||||||
if (status != noErr) {
|
|
||||||
fprintf(stderr, "Error in LSRegisterURL: %d\n", (int)status);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void Discord_Register(const char *applicationId, const char *command) {
|
|
||||||
|
|
||||||
if (command) {
|
|
||||||
RegisterCommand(applicationId, command);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// raii lite
|
|
||||||
@autoreleasepool {
|
|
||||||
RegisterURL(applicationId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
165
3rdparty/discord-rpc/discord_register_win.cpp
vendored
165
3rdparty/discord-rpc/discord_register_win.cpp
vendored
@@ -1,165 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2017 Discord, Inc.
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
* this software and associated documentation files (the "Software"), to deal in
|
|
||||||
* the Software without restriction, including without limitation the rights to
|
|
||||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
||||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
|
||||||
* so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "discord_rpc.h"
|
|
||||||
#include "discord_register.h"
|
|
||||||
|
|
||||||
#define WIN32_LEAN_AND_MEAN
|
|
||||||
#define NOMCX
|
|
||||||
#define NOSERVICE
|
|
||||||
#define NOIME
|
|
||||||
|
|
||||||
#include <windows.h>
|
|
||||||
#include <psapi.h>
|
|
||||||
#include <cstdio>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updated fixes for MinGW and WinXP
|
|
||||||
* This block is written the way it does not involve changing the rest of the code
|
|
||||||
* Checked to be compiling
|
|
||||||
* 1) strsafe.h belongs to Windows SDK and cannot be added to MinGW
|
|
||||||
* #include guarded, functions redirected to <string.h> substitutes
|
|
||||||
* 2) RegSetKeyValueW and LSTATUS are not declared in <winreg.h>
|
|
||||||
* The entire function is rewritten
|
|
||||||
*/
|
|
||||||
#ifdef __MINGW32__
|
|
||||||
# include <wchar.h>
|
|
||||||
/// strsafe.h fixes
|
|
||||||
static HRESULT StringCbPrintfW(LPWSTR pszDest, size_t cbDest, LPCWSTR pszFormat, ...) {
|
|
||||||
HRESULT ret;
|
|
||||||
va_list va;
|
|
||||||
va_start(va, pszFormat);
|
|
||||||
cbDest /= 2; // Size is divided by 2 to convert from bytes to wide characters - causes segfault
|
|
||||||
// othervise
|
|
||||||
ret = vsnwprintf(pszDest, cbDest, pszFormat, va);
|
|
||||||
pszDest[cbDest - 1] = 0; // Terminate the string in case a buffer overflow; -1 will be returned
|
|
||||||
va_end(va);
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
# include <cwchar>
|
|
||||||
# include <strsafe.h>
|
|
||||||
#endif // __MINGW32__
|
|
||||||
|
|
||||||
/// winreg.h fixes
|
|
||||||
#ifndef LSTATUS
|
|
||||||
# define LSTATUS LONG
|
|
||||||
#endif
|
|
||||||
#ifdef RegSetKeyValueW
|
|
||||||
# undefine RegSetKeyValueW
|
|
||||||
#endif
|
|
||||||
#define RegSetKeyValueW regset
|
|
||||||
|
|
||||||
static LSTATUS regset(HKEY hkey, LPCWSTR subkey, LPCWSTR name, DWORD type, const void *data, DWORD len) {
|
|
||||||
|
|
||||||
HKEY htkey = hkey, hsubkey = nullptr;
|
|
||||||
LSTATUS ret;
|
|
||||||
if (subkey && subkey[0]) {
|
|
||||||
if ((ret = RegCreateKeyExW(hkey, subkey, 0, 0, 0, KEY_ALL_ACCESS, 0, &hsubkey, 0)) !=
|
|
||||||
ERROR_SUCCESS)
|
|
||||||
return ret;
|
|
||||||
htkey = hsubkey;
|
|
||||||
}
|
|
||||||
ret = RegSetValueExW(htkey, name, 0, type, static_cast<const BYTE*>(data), len);
|
|
||||||
if (hsubkey && hsubkey != hkey)
|
|
||||||
RegCloseKey(hsubkey);
|
|
||||||
return ret;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
static void Discord_RegisterW(const wchar_t *applicationId, const wchar_t *command) {
|
|
||||||
|
|
||||||
// https://msdn.microsoft.com/en-us/library/aa767914(v=vs.85).aspx
|
|
||||||
// we want to register games so we can run them as discord-<appid>://
|
|
||||||
// Update the HKEY_CURRENT_USER, because it doesn't seem to require special permissions.
|
|
||||||
|
|
||||||
wchar_t exeFilePath[MAX_PATH]{};
|
|
||||||
DWORD exeLen = GetModuleFileNameW(nullptr, exeFilePath, MAX_PATH);
|
|
||||||
wchar_t openCommand[1024]{};
|
|
||||||
|
|
||||||
if (command && command[0]) {
|
|
||||||
StringCbPrintfW(openCommand, sizeof(openCommand), L"%s", command);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// StringCbCopyW(openCommand, sizeof(openCommand), exeFilePath);
|
|
||||||
StringCbPrintfW(openCommand, sizeof(openCommand), L"%s", exeFilePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
wchar_t protocolName[64]{};
|
|
||||||
StringCbPrintfW(protocolName, sizeof(protocolName), L"discord-%s", applicationId);
|
|
||||||
wchar_t protocolDescription[128]{};
|
|
||||||
StringCbPrintfW(protocolDescription, sizeof(protocolDescription), L"URL:Run game %s protocol", applicationId);
|
|
||||||
wchar_t urlProtocol = 0;
|
|
||||||
|
|
||||||
wchar_t keyName[256]{};
|
|
||||||
StringCbPrintfW(keyName, sizeof(keyName), L"Software\\Classes\\%s", protocolName);
|
|
||||||
HKEY key;
|
|
||||||
auto status = RegCreateKeyExW(HKEY_CURRENT_USER, keyName, 0, nullptr, 0, KEY_WRITE, nullptr, &key, nullptr);
|
|
||||||
if (status != ERROR_SUCCESS) {
|
|
||||||
fprintf(stderr, "Error creating key\n");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
DWORD len;
|
|
||||||
LSTATUS result;
|
|
||||||
len = static_cast<DWORD>(lstrlenW(protocolDescription) + 1);
|
|
||||||
result = RegSetKeyValueW(key, nullptr, nullptr, REG_SZ, protocolDescription, len * sizeof(wchar_t));
|
|
||||||
if (FAILED(result)) {
|
|
||||||
fprintf(stderr, "Error writing description\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
len = static_cast<DWORD>(lstrlenW(protocolDescription) + 1);
|
|
||||||
result = RegSetKeyValueW(key, nullptr, L"URL Protocol", REG_SZ, &urlProtocol, sizeof(wchar_t));
|
|
||||||
if (FAILED(result)) {
|
|
||||||
fprintf(stderr, "Error writing description\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
result = RegSetKeyValueW(key, L"DefaultIcon", nullptr, REG_SZ, exeFilePath, (exeLen + 1) * sizeof(wchar_t));
|
|
||||||
if (FAILED(result)) {
|
|
||||||
fprintf(stderr, "Error writing icon\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
len = static_cast<DWORD>(lstrlenW(openCommand) + 1);
|
|
||||||
result = RegSetKeyValueW(key, L"shell\\open\\command", nullptr, REG_SZ, openCommand, len * sizeof(wchar_t));
|
|
||||||
if (FAILED(result)) {
|
|
||||||
fprintf(stderr, "Error writing command\n");
|
|
||||||
}
|
|
||||||
RegCloseKey(key);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" void Discord_Register(const char *applicationId, const char *command) {
|
|
||||||
|
|
||||||
wchar_t appId[32]{};
|
|
||||||
MultiByteToWideChar(CP_UTF8, 0, applicationId, -1, appId, 32);
|
|
||||||
|
|
||||||
wchar_t openCommand[1024]{};
|
|
||||||
const wchar_t *wcommand = nullptr;
|
|
||||||
if (command && command[0]) {
|
|
||||||
const auto commandBufferLen = sizeof(openCommand) / sizeof(*openCommand);
|
|
||||||
MultiByteToWideChar(CP_UTF8, 0, command, -1, openCommand, commandBufferLen);
|
|
||||||
wcommand = openCommand;
|
|
||||||
}
|
|
||||||
|
|
||||||
Discord_RegisterW(appId, wcommand);
|
|
||||||
|
|
||||||
}
|
|
||||||
510
3rdparty/discord-rpc/discord_rpc.cpp
vendored
510
3rdparty/discord-rpc/discord_rpc.cpp
vendored
@@ -1,510 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2017 Discord, Inc.
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
* this software and associated documentation files (the "Software"), to deal in
|
|
||||||
* the Software without restriction, including without limitation the rights to
|
|
||||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
||||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
|
||||||
* so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <atomic>
|
|
||||||
#include <chrono>
|
|
||||||
#include <mutex>
|
|
||||||
#include <condition_variable>
|
|
||||||
#include <thread>
|
|
||||||
|
|
||||||
#include "discord_rpc.h"
|
|
||||||
#include "discord_backoff.h"
|
|
||||||
#include "discord_register.h"
|
|
||||||
#include "discord_msg_queue.h"
|
|
||||||
#include "discord_rpc_connection.h"
|
|
||||||
#include "discord_serialization.h"
|
|
||||||
|
|
||||||
using namespace discord_rpc;
|
|
||||||
|
|
||||||
static void Discord_UpdateConnection();
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
constexpr size_t MaxMessageSize{ 16 * 1024 };
|
|
||||||
constexpr size_t MessageQueueSize{ 8 };
|
|
||||||
constexpr size_t JoinQueueSize{ 8 };
|
|
||||||
|
|
||||||
struct QueuedMessage {
|
|
||||||
size_t length;
|
|
||||||
char buffer[MaxMessageSize];
|
|
||||||
|
|
||||||
void Copy(const QueuedMessage &other) {
|
|
||||||
length = other.length;
|
|
||||||
if (length) {
|
|
||||||
memcpy(buffer, other.buffer, length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
struct User {
|
|
||||||
// snowflake (64bit int), turned into a ascii decimal string, at most 20 chars +1 null
|
|
||||||
// terminator = 21
|
|
||||||
char userId[32];
|
|
||||||
// 32 unicode glyphs is max name size => 4 bytes per glyph in the worst case, +1 for null
|
|
||||||
// terminator = 129
|
|
||||||
char username[344];
|
|
||||||
// 4 decimal digits + 1 null terminator = 5
|
|
||||||
char discriminator[8];
|
|
||||||
// optional 'a_' + md5 hex digest (32 bytes) + null terminator = 35
|
|
||||||
char avatar[128];
|
|
||||||
// Rounded way up because I'm paranoid about games breaking from future changes in these sizes
|
|
||||||
};
|
|
||||||
|
|
||||||
static RpcConnection *Connection{ nullptr };
|
|
||||||
static DiscordEventHandlers QueuedHandlers{};
|
|
||||||
static DiscordEventHandlers Handlers{};
|
|
||||||
static std::atomic_bool WasJustConnected{ false };
|
|
||||||
static std::atomic_bool WasJustDisconnected{ false };
|
|
||||||
static std::atomic_bool GotErrorMessage{ false };
|
|
||||||
static std::atomic_bool WasJoinGame{ false };
|
|
||||||
static std::atomic_bool WasSpectateGame{ false };
|
|
||||||
static std::atomic_bool UpdatePresence{ false };
|
|
||||||
static char JoinGameSecret[256];
|
|
||||||
static char SpectateGameSecret[256];
|
|
||||||
static int LastErrorCode{ 0 };
|
|
||||||
static char LastErrorMessage[256];
|
|
||||||
static int LastDisconnectErrorCode{ 0 };
|
|
||||||
static char LastDisconnectErrorMessage[256];
|
|
||||||
static std::mutex PresenceMutex;
|
|
||||||
static std::mutex HandlerMutex;
|
|
||||||
static QueuedMessage QueuedPresence{};
|
|
||||||
static MsgQueue<QueuedMessage, MessageQueueSize> SendQueue;
|
|
||||||
static MsgQueue<User, JoinQueueSize> JoinAskQueue;
|
|
||||||
static User connectedUser;
|
|
||||||
|
|
||||||
// We want to auto connect, and retry on failure, but not as fast as possible. This does expoential backoff from 0.5 seconds to 1 minute
|
|
||||||
static Backoff ReconnectTimeMs(500, 60 * 1000);
|
|
||||||
static auto NextConnect = std::chrono::system_clock::now();
|
|
||||||
static int Pid{ 0 };
|
|
||||||
static int Nonce{ 1 };
|
|
||||||
|
|
||||||
class IoThreadHolder {
|
|
||||||
private:
|
|
||||||
std::atomic_bool keepRunning{ true };
|
|
||||||
std::mutex waitForIOMutex;
|
|
||||||
std::condition_variable waitForIOActivity;
|
|
||||||
std::thread ioThread;
|
|
||||||
|
|
||||||
public:
|
|
||||||
void Start() {
|
|
||||||
keepRunning.store(true);
|
|
||||||
ioThread = std::thread([&]() {
|
|
||||||
const std::chrono::duration<int64_t, std::milli> maxWait { 500LL };
|
|
||||||
Discord_UpdateConnection();
|
|
||||||
while (keepRunning.load()) {
|
|
||||||
std::unique_lock<std::mutex> lock(waitForIOMutex);
|
|
||||||
waitForIOActivity.wait_for(lock, maxWait);
|
|
||||||
Discord_UpdateConnection();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void Notify() { waitForIOActivity.notify_all(); }
|
|
||||||
|
|
||||||
void Stop() {
|
|
||||||
keepRunning.exchange(false);
|
|
||||||
Notify();
|
|
||||||
if (ioThread.joinable()) {
|
|
||||||
ioThread.join();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
~IoThreadHolder() { Stop(); }
|
|
||||||
};
|
|
||||||
|
|
||||||
static IoThreadHolder *IoThread{ nullptr };
|
|
||||||
|
|
||||||
static void UpdateReconnectTime() {
|
|
||||||
|
|
||||||
NextConnect = std::chrono::system_clock::now() + std::chrono::duration<int64_t, std::milli> { ReconnectTimeMs.nextDelay() };
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
static void SignalIOActivity() {
|
|
||||||
|
|
||||||
if (IoThread != nullptr) {
|
|
||||||
IoThread->Notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool RegisterForEvent(const char *evtName) {
|
|
||||||
|
|
||||||
auto qmessage = SendQueue.GetNextAddMessage();
|
|
||||||
if (qmessage) {
|
|
||||||
qmessage->length = JsonWriteSubscribeCommand(qmessage->buffer, sizeof(qmessage->buffer), Nonce++, evtName);
|
|
||||||
SendQueue.CommitAdd();
|
|
||||||
SignalIOActivity();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool DeregisterForEvent(const char *evtName) {
|
|
||||||
|
|
||||||
auto qmessage = SendQueue.GetNextAddMessage();
|
|
||||||
if (qmessage) {
|
|
||||||
qmessage->length = JsonWriteUnsubscribeCommand(qmessage->buffer, sizeof(qmessage->buffer), Nonce++, evtName);
|
|
||||||
SendQueue.CommitAdd();
|
|
||||||
SignalIOActivity();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
static void Discord_UpdateConnection() {
|
|
||||||
|
|
||||||
if (!Connection) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Connection->IsOpen()) {
|
|
||||||
if (std::chrono::system_clock::now() >= NextConnect) {
|
|
||||||
UpdateReconnectTime();
|
|
||||||
Connection->Open();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// reads
|
|
||||||
|
|
||||||
for (;;) {
|
|
||||||
JsonDocument message;
|
|
||||||
|
|
||||||
if (!Connection->Read(message)) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const char *evtName = GetStrMember(&message, "evt");
|
|
||||||
const char *nonce = GetStrMember(&message, "nonce");
|
|
||||||
|
|
||||||
if (nonce) {
|
|
||||||
// in responses only -- should use to match up response when needed.
|
|
||||||
|
|
||||||
if (evtName && strcmp(evtName, "ERROR") == 0) {
|
|
||||||
auto data = GetObjMember(&message, "data");
|
|
||||||
LastErrorCode = GetIntMember(data, "code");
|
|
||||||
StringCopy(LastErrorMessage, GetStrMember(data, "message", ""));
|
|
||||||
GotErrorMessage.store(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// should have evt == name of event, optional data
|
|
||||||
if (evtName == nullptr) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto data = GetObjMember(&message, "data");
|
|
||||||
|
|
||||||
if (strcmp(evtName, "ACTIVITY_JOIN") == 0) {
|
|
||||||
auto secret = GetStrMember(data, "secret");
|
|
||||||
if (secret) {
|
|
||||||
StringCopy(JoinGameSecret, secret);
|
|
||||||
WasJoinGame.store(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (strcmp(evtName, "ACTIVITY_SPECTATE") == 0) {
|
|
||||||
auto secret = GetStrMember(data, "secret");
|
|
||||||
if (secret) {
|
|
||||||
StringCopy(SpectateGameSecret, secret);
|
|
||||||
WasSpectateGame.store(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (strcmp(evtName, "ACTIVITY_JOIN_REQUEST") == 0) {
|
|
||||||
auto user = GetObjMember(data, "user");
|
|
||||||
auto userId = GetStrMember(user, "id");
|
|
||||||
auto username = GetStrMember(user, "username");
|
|
||||||
auto avatar = GetStrMember(user, "avatar");
|
|
||||||
auto joinReq = JoinAskQueue.GetNextAddMessage();
|
|
||||||
if (userId && username && joinReq) {
|
|
||||||
StringCopy(joinReq->userId, userId);
|
|
||||||
StringCopy(joinReq->username, username);
|
|
||||||
auto discriminator = GetStrMember(user, "discriminator");
|
|
||||||
if (discriminator) {
|
|
||||||
StringCopy(joinReq->discriminator, discriminator);
|
|
||||||
}
|
|
||||||
if (avatar) {
|
|
||||||
StringCopy(joinReq->avatar, avatar);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
joinReq->avatar[0] = 0;
|
|
||||||
}
|
|
||||||
JoinAskQueue.CommitAdd();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// writes
|
|
||||||
if (UpdatePresence.exchange(false) && QueuedPresence.length) {
|
|
||||||
QueuedMessage local;
|
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> guard(PresenceMutex);
|
|
||||||
local.Copy(QueuedPresence);
|
|
||||||
}
|
|
||||||
if (!Connection->Write(local.buffer, local.length)) {
|
|
||||||
// if we fail to send, requeue
|
|
||||||
std::lock_guard<std::mutex> guard(PresenceMutex);
|
|
||||||
QueuedPresence.Copy(local);
|
|
||||||
UpdatePresence.exchange(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
while (SendQueue.HavePendingSends()) {
|
|
||||||
auto qmessage = SendQueue.GetNextSendMessage();
|
|
||||||
Connection->Write(qmessage->buffer, qmessage->length);
|
|
||||||
SendQueue.CommitSend();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" void Discord_Initialize(const char *applicationId, DiscordEventHandlers *handlers, const int autoRegister) {
|
|
||||||
|
|
||||||
IoThread = new (std::nothrow) IoThreadHolder();
|
|
||||||
if (IoThread == nullptr) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (autoRegister) {
|
|
||||||
Discord_Register(applicationId, nullptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
Pid = GetProcessId();
|
|
||||||
|
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
|
||||||
|
|
||||||
if (handlers) {
|
|
||||||
QueuedHandlers = *handlers;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
QueuedHandlers = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
Handlers = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Connection) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Connection = RpcConnection::Create(applicationId);
|
|
||||||
Connection->onConnect = [](JsonDocument &readyMessage) {
|
|
||||||
Discord_UpdateHandlers(&QueuedHandlers);
|
|
||||||
if (QueuedPresence.length > 0) {
|
|
||||||
UpdatePresence.exchange(true);
|
|
||||||
SignalIOActivity();
|
|
||||||
}
|
|
||||||
auto data = GetObjMember(&readyMessage, "data");
|
|
||||||
auto user = GetObjMember(data, "user");
|
|
||||||
auto userId = GetStrMember(user, "id");
|
|
||||||
auto username = GetStrMember(user, "username");
|
|
||||||
auto avatar = GetStrMember(user, "avatar");
|
|
||||||
if (userId && username) {
|
|
||||||
StringCopy(connectedUser.userId, userId);
|
|
||||||
StringCopy(connectedUser.username, username);
|
|
||||||
auto discriminator = GetStrMember(user, "discriminator");
|
|
||||||
if (discriminator) {
|
|
||||||
StringCopy(connectedUser.discriminator, discriminator);
|
|
||||||
}
|
|
||||||
if (avatar) {
|
|
||||||
StringCopy(connectedUser.avatar, avatar);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
connectedUser.avatar[0] = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
WasJustConnected.exchange(true);
|
|
||||||
ReconnectTimeMs.reset();
|
|
||||||
};
|
|
||||||
Connection->onDisconnect = [](int err, const char *message) {
|
|
||||||
LastDisconnectErrorCode = err;
|
|
||||||
StringCopy(LastDisconnectErrorMessage, message);
|
|
||||||
WasJustDisconnected.exchange(true);
|
|
||||||
UpdateReconnectTime();
|
|
||||||
};
|
|
||||||
|
|
||||||
IoThread->Start();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" void Discord_Shutdown() {
|
|
||||||
|
|
||||||
if (!Connection) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Connection->onConnect = nullptr;
|
|
||||||
Connection->onDisconnect = nullptr;
|
|
||||||
Handlers = {};
|
|
||||||
QueuedPresence.length = 0;
|
|
||||||
UpdatePresence.exchange(false);
|
|
||||||
if (IoThread != nullptr) {
|
|
||||||
IoThread->Stop();
|
|
||||||
delete IoThread;
|
|
||||||
IoThread = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
RpcConnection::Destroy(Connection);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" void Discord_UpdatePresence(const DiscordRichPresence *presence) {
|
|
||||||
|
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> guard(PresenceMutex);
|
|
||||||
QueuedPresence.length = JsonWriteRichPresenceObj(QueuedPresence.buffer, sizeof(QueuedPresence.buffer), Nonce++, Pid, presence);
|
|
||||||
UpdatePresence.exchange(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
SignalIOActivity();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" void Discord_ClearPresence(void) {
|
|
||||||
Discord_UpdatePresence(nullptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" void Discord_Respond(const char *userId, /* DISCORD_REPLY_ */ int reply) {
|
|
||||||
|
|
||||||
// if we are not connected, let's not batch up stale messages for later
|
|
||||||
if (!Connection || !Connection->IsOpen()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
auto qmessage = SendQueue.GetNextAddMessage();
|
|
||||||
if (qmessage) {
|
|
||||||
qmessage->length = JsonWriteJoinReply(qmessage->buffer, sizeof(qmessage->buffer), userId, reply, Nonce++);
|
|
||||||
SendQueue.CommitAdd();
|
|
||||||
SignalIOActivity();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" void Discord_RunCallbacks() {
|
|
||||||
|
|
||||||
// Note on some weirdness: internally we might connect, get other signals, disconnect any number
|
|
||||||
// of times inbetween calls here. Externally, we want the sequence to seem sane, so any other
|
|
||||||
// signals are book-ended by calls to ready and disconnect.
|
|
||||||
|
|
||||||
if (!Connection) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const bool wasDisconnected = WasJustDisconnected.exchange(false);
|
|
||||||
const bool isConnected = Connection->IsOpen();
|
|
||||||
|
|
||||||
if (isConnected) {
|
|
||||||
// if we are connected, disconnect cb first
|
|
||||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
|
||||||
if (wasDisconnected && Handlers.disconnected) {
|
|
||||||
Handlers.disconnected(LastDisconnectErrorCode, LastDisconnectErrorMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (WasJustConnected.exchange(false)) {
|
|
||||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
|
||||||
if (Handlers.ready) {
|
|
||||||
DiscordUser du{ connectedUser.userId, connectedUser.username, connectedUser.discriminator, connectedUser.avatar };
|
|
||||||
Handlers.ready(&du);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (GotErrorMessage.exchange(false)) {
|
|
||||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
|
||||||
if (Handlers.errored) {
|
|
||||||
Handlers.errored(LastErrorCode, LastErrorMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (WasJoinGame.exchange(false)) {
|
|
||||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
|
||||||
if (Handlers.joinGame) {
|
|
||||||
Handlers.joinGame(JoinGameSecret);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (WasSpectateGame.exchange(false)) {
|
|
||||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
|
||||||
if (Handlers.spectateGame) {
|
|
||||||
Handlers.spectateGame(SpectateGameSecret);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Right now this batches up any requests and sends them all in a burst; I could imagine a world
|
|
||||||
// where the implementer would rather sequentially accept/reject each one before the next invite
|
|
||||||
// is sent. I left it this way because I could also imagine wanting to process these all and
|
|
||||||
// maybe show them in one common dialog and/or start fetching the avatars in parallel, and if
|
|
||||||
// not it should be trivial for the implementer to make a queue themselves.
|
|
||||||
while (JoinAskQueue.HavePendingSends()) {
|
|
||||||
const auto req = JoinAskQueue.GetNextSendMessage();
|
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
|
||||||
if (Handlers.joinRequest) {
|
|
||||||
DiscordUser du{ req->userId, req->username, req->discriminator, req->avatar };
|
|
||||||
Handlers.joinRequest(&du);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
JoinAskQueue.CommitSend();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isConnected) {
|
|
||||||
// if we are not connected, disconnect message last
|
|
||||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
|
||||||
if (wasDisconnected && Handlers.disconnected) {
|
|
||||||
Handlers.disconnected(LastDisconnectErrorCode, LastDisconnectErrorMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" void Discord_UpdateHandlers(DiscordEventHandlers *newHandlers) {
|
|
||||||
|
|
||||||
if (newHandlers) {
|
|
||||||
#define HANDLE_EVENT_REGISTRATION(handler_name, event) \
|
|
||||||
if (!Handlers.handler_name && newHandlers->handler_name) { \
|
|
||||||
RegisterForEvent(event); \
|
|
||||||
} \
|
|
||||||
else if (Handlers.handler_name && !newHandlers->handler_name) { \
|
|
||||||
DeregisterForEvent(event); \
|
|
||||||
}
|
|
||||||
|
|
||||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
|
||||||
HANDLE_EVENT_REGISTRATION(joinGame, "ACTIVITY_JOIN")
|
|
||||||
HANDLE_EVENT_REGISTRATION(spectateGame, "ACTIVITY_SPECTATE")
|
|
||||||
HANDLE_EVENT_REGISTRATION(joinRequest, "ACTIVITY_JOIN_REQUEST")
|
|
||||||
|
|
||||||
#undef HANDLE_EVENT_REGISTRATION
|
|
||||||
|
|
||||||
Handlers = *newHandlers;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
std::lock_guard<std::mutex> guard(HandlerMutex);
|
|
||||||
Handlers = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
94
3rdparty/discord-rpc/discord_rpc.h
vendored
94
3rdparty/discord-rpc/discord_rpc.h
vendored
@@ -1,94 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2017 Discord, Inc.
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
* this software and associated documentation files (the "Software"), to deal in
|
|
||||||
* the Software without restriction, including without limitation the rights to
|
|
||||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
||||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
|
||||||
* so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef DISCORD_RPC_H
|
|
||||||
#define DISCORD_RPC_H
|
|
||||||
|
|
||||||
#include <cstdint>
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
extern "C" {
|
|
||||||
#endif
|
|
||||||
|
|
||||||
typedef struct DiscordRichPresence {
|
|
||||||
int type;
|
|
||||||
int status_display_type;
|
|
||||||
const char *name; /* max 128 bytes */
|
|
||||||
const char *state; /* max 128 bytes */
|
|
||||||
const char *details; /* max 128 bytes */
|
|
||||||
int64_t startTimestamp;
|
|
||||||
int64_t endTimestamp;
|
|
||||||
const char *largeImageKey; /* max 32 bytes */
|
|
||||||
const char *largeImageText; /* max 128 bytes */
|
|
||||||
const char *smallImageKey; /* max 32 bytes */
|
|
||||||
const char *smallImageText; /* max 128 bytes */
|
|
||||||
const char *partyId; /* max 128 bytes */
|
|
||||||
int partySize;
|
|
||||||
int partyMax;
|
|
||||||
int partyPrivacy;
|
|
||||||
const char *matchSecret; /* max 128 bytes */
|
|
||||||
const char *joinSecret; /* max 128 bytes */
|
|
||||||
const char *spectateSecret; /* max 128 bytes */
|
|
||||||
int8_t instance;
|
|
||||||
} DiscordRichPresence;
|
|
||||||
|
|
||||||
typedef struct DiscordUser {
|
|
||||||
const char *userId;
|
|
||||||
const char *username;
|
|
||||||
const char *discriminator;
|
|
||||||
const char *avatar;
|
|
||||||
} DiscordUser;
|
|
||||||
|
|
||||||
typedef struct DiscordEventHandlers {
|
|
||||||
void (*ready)(const DiscordUser *request);
|
|
||||||
void (*disconnected)(int errorCode, const char *message);
|
|
||||||
void (*errored)(int errorCode, const char *message);
|
|
||||||
void (*joinGame)(const char *joinSecret);
|
|
||||||
void (*spectateGame)(const char *spectateSecret);
|
|
||||||
void (*joinRequest)(const DiscordUser *request);
|
|
||||||
} DiscordEventHandlers;
|
|
||||||
|
|
||||||
#define DISCORD_REPLY_NO 0
|
|
||||||
#define DISCORD_REPLY_YES 1
|
|
||||||
#define DISCORD_REPLY_IGNORE 2
|
|
||||||
#define DISCORD_PARTY_PRIVATE 0
|
|
||||||
#define DISCORD_PARTY_PUBLIC 1
|
|
||||||
|
|
||||||
void Discord_Initialize(const char *applicationId, DiscordEventHandlers *handlers, const int autoRegister);
|
|
||||||
void Discord_Shutdown(void);
|
|
||||||
|
|
||||||
// checks for incoming messages, dispatches callbacks
|
|
||||||
void Discord_RunCallbacks(void);
|
|
||||||
|
|
||||||
void Discord_UpdatePresence(const DiscordRichPresence *presence);
|
|
||||||
void Discord_ClearPresence(void);
|
|
||||||
|
|
||||||
void Discord_Respond(const char *userid, /* DISCORD_REPLY_ */ int reply);
|
|
||||||
|
|
||||||
void Discord_UpdateHandlers(DiscordEventHandlers *handlers);
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
} /* extern "C" */
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#endif // DISCORD_RPC_H
|
|
||||||
168
3rdparty/discord-rpc/discord_rpc_connection.cpp
vendored
168
3rdparty/discord-rpc/discord_rpc_connection.cpp
vendored
@@ -1,168 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2017 Discord, Inc.
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
* this software and associated documentation files (the "Software"), to deal in
|
|
||||||
* the Software without restriction, including without limitation the rights to
|
|
||||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
||||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
|
||||||
* so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "discord_rpc_connection.h"
|
|
||||||
#include "discord_serialization.h"
|
|
||||||
|
|
||||||
namespace discord_rpc {
|
|
||||||
|
|
||||||
static const int RpcVersion = 1;
|
|
||||||
static RpcConnection Instance;
|
|
||||||
|
|
||||||
RpcConnection *RpcConnection::Create(const char *applicationId) {
|
|
||||||
|
|
||||||
Instance.connection = BaseConnection::Create();
|
|
||||||
StringCopy(Instance.appId, applicationId);
|
|
||||||
return &Instance;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void RpcConnection::Destroy(RpcConnection *&c) {
|
|
||||||
|
|
||||||
c->Close();
|
|
||||||
BaseConnection::Destroy(c->connection);
|
|
||||||
c = nullptr;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void RpcConnection::Open() {
|
|
||||||
|
|
||||||
if (state == State::Connected) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state == State::Disconnected && !connection->Open()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state == State::SentHandshake) {
|
|
||||||
JsonDocument message;
|
|
||||||
if (Read(message)) {
|
|
||||||
auto cmd = GetStrMember(&message, "cmd");
|
|
||||||
auto evt = GetStrMember(&message, "evt");
|
|
||||||
if (cmd && evt && !strcmp(cmd, "DISPATCH") && !strcmp(evt, "READY")) {
|
|
||||||
state = State::Connected;
|
|
||||||
if (onConnect) {
|
|
||||||
onConnect(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
sendFrame.opcode = Opcode::Handshake;
|
|
||||||
sendFrame.length = static_cast<uint32_t>(JsonWriteHandshakeObj(sendFrame.message, sizeof(sendFrame.message), RpcVersion, appId));
|
|
||||||
|
|
||||||
if (connection->Write(&sendFrame, sizeof(MessageFrameHeader) + sendFrame.length)) {
|
|
||||||
state = State::SentHandshake;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void RpcConnection::Close() {
|
|
||||||
|
|
||||||
if (onDisconnect && (state == State::Connected || state == State::SentHandshake)) {
|
|
||||||
onDisconnect(lastErrorCode, lastErrorMessage);
|
|
||||||
}
|
|
||||||
connection->Close();
|
|
||||||
state = State::Disconnected;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RpcConnection::Write(const void *data, size_t length) {
|
|
||||||
|
|
||||||
sendFrame.opcode = Opcode::Frame;
|
|
||||||
memcpy(sendFrame.message, data, length);
|
|
||||||
sendFrame.length = static_cast<uint32_t>(length);
|
|
||||||
if (!connection->Write(&sendFrame, sizeof(MessageFrameHeader) + length)) {
|
|
||||||
Close();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RpcConnection::Read(JsonDocument &message) {
|
|
||||||
|
|
||||||
if (state != State::Connected && state != State::SentHandshake) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
MessageFrame readFrame{};
|
|
||||||
for (;;) {
|
|
||||||
bool didRead = connection->Read(&readFrame, sizeof(MessageFrameHeader));
|
|
||||||
if (!didRead) {
|
|
||||||
if (!connection->isOpen) {
|
|
||||||
lastErrorCode = static_cast<int>(ErrorCode::PipeClosed);
|
|
||||||
StringCopy(lastErrorMessage, "Pipe closed");
|
|
||||||
Close();
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (readFrame.length > 0) {
|
|
||||||
didRead = connection->Read(readFrame.message, readFrame.length);
|
|
||||||
if (!didRead) {
|
|
||||||
lastErrorCode = static_cast<int>(ErrorCode::ReadCorrupt);
|
|
||||||
StringCopy(lastErrorMessage, "Partial data in frame");
|
|
||||||
Close();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
readFrame.message[readFrame.length] = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (readFrame.opcode) {
|
|
||||||
case Opcode::Close: {
|
|
||||||
message.ParseInsitu(readFrame.message);
|
|
||||||
lastErrorCode = GetIntMember(&message, "code");
|
|
||||||
StringCopy(lastErrorMessage, GetStrMember(&message, "message", ""));
|
|
||||||
Close();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
case Opcode::Frame:
|
|
||||||
message.ParseInsitu(readFrame.message);
|
|
||||||
return true;
|
|
||||||
case Opcode::Ping:
|
|
||||||
readFrame.opcode = Opcode::Pong;
|
|
||||||
if (!connection->Write(&readFrame, sizeof(MessageFrameHeader) + readFrame.length)) {
|
|
||||||
Close();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case Opcode::Pong:
|
|
||||||
break;
|
|
||||||
case Opcode::Handshake:
|
|
||||||
default:
|
|
||||||
// something bad happened
|
|
||||||
lastErrorCode = static_cast<int>(ErrorCode::ReadCorrupt);
|
|
||||||
StringCopy(lastErrorMessage, "Bad ipc frame");
|
|
||||||
Close();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace discord_rpc
|
|
||||||
88
3rdparty/discord-rpc/discord_rpc_connection.h
vendored
88
3rdparty/discord-rpc/discord_rpc_connection.h
vendored
@@ -1,88 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2017 Discord, Inc.
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
* this software and associated documentation files (the "Software"), to deal in
|
|
||||||
* the Software without restriction, including without limitation the rights to
|
|
||||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
||||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
|
||||||
* so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef DISCORD_RPC_CONNECTION_H
|
|
||||||
#define DISCORD_RPC_CONNECTION_H
|
|
||||||
|
|
||||||
#include "discord_connection.h"
|
|
||||||
#include "discord_serialization.h"
|
|
||||||
|
|
||||||
namespace discord_rpc {
|
|
||||||
|
|
||||||
// I took this from the buffer size libuv uses for named pipes; I suspect ours would usually be much smaller.
|
|
||||||
constexpr size_t MaxRpcFrameSize = 64 * 1024;
|
|
||||||
|
|
||||||
struct RpcConnection {
|
|
||||||
enum class ErrorCode : int {
|
|
||||||
Success = 0,
|
|
||||||
PipeClosed = 1,
|
|
||||||
ReadCorrupt = 2,
|
|
||||||
};
|
|
||||||
|
|
||||||
enum class Opcode : uint32_t {
|
|
||||||
Handshake = 0,
|
|
||||||
Frame = 1,
|
|
||||||
Close = 2,
|
|
||||||
Ping = 3,
|
|
||||||
Pong = 4,
|
|
||||||
};
|
|
||||||
|
|
||||||
struct MessageFrameHeader {
|
|
||||||
Opcode opcode;
|
|
||||||
uint32_t length;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct MessageFrame : public MessageFrameHeader {
|
|
||||||
char message[MaxRpcFrameSize - sizeof(MessageFrameHeader)];
|
|
||||||
};
|
|
||||||
|
|
||||||
enum class State : uint32_t {
|
|
||||||
Disconnected,
|
|
||||||
SentHandshake,
|
|
||||||
AwaitingResponse,
|
|
||||||
Connected,
|
|
||||||
};
|
|
||||||
|
|
||||||
BaseConnection *connection{ nullptr };
|
|
||||||
State state{ State::Disconnected };
|
|
||||||
void (*onConnect)(JsonDocument &message) { nullptr };
|
|
||||||
void (*onDisconnect)(int errorCode, const char *message) { nullptr };
|
|
||||||
char appId[64]{};
|
|
||||||
int lastErrorCode{ 0 };
|
|
||||||
char lastErrorMessage[256]{};
|
|
||||||
RpcConnection::MessageFrame sendFrame;
|
|
||||||
|
|
||||||
static RpcConnection *Create(const char *applicationId);
|
|
||||||
static void Destroy(RpcConnection*&);
|
|
||||||
|
|
||||||
inline bool IsOpen() const { return state == State::Connected; }
|
|
||||||
|
|
||||||
void Open();
|
|
||||||
void Close();
|
|
||||||
bool Write(const void *data, size_t length);
|
|
||||||
bool Read(JsonDocument &message);
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace discord_rpc
|
|
||||||
|
|
||||||
#endif // DISCORD_RPC_CONNECTION_H
|
|
||||||
285
3rdparty/discord-rpc/discord_serialization.cpp
vendored
285
3rdparty/discord-rpc/discord_serialization.cpp
vendored
@@ -1,285 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2017 Discord, Inc.
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
* this software and associated documentation files (the "Software"), to deal in
|
|
||||||
* the Software without restriction, including without limitation the rights to
|
|
||||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
||||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
|
||||||
* so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "discord_serialization.h"
|
|
||||||
#include "discord_connection.h"
|
|
||||||
#include "discord_rpc.h"
|
|
||||||
|
|
||||||
namespace discord_rpc {
|
|
||||||
|
|
||||||
template<typename T>
|
|
||||||
void NumberToString(char *dest, T number) {
|
|
||||||
|
|
||||||
if (!number) {
|
|
||||||
*dest++ = '0';
|
|
||||||
*dest++ = 0;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (number < 0) {
|
|
||||||
*dest++ = '-';
|
|
||||||
number = -number;
|
|
||||||
}
|
|
||||||
char temp[32];
|
|
||||||
int place = 0;
|
|
||||||
while (number) {
|
|
||||||
auto digit = number % 10;
|
|
||||||
number = number / 10;
|
|
||||||
temp[place++] = '0' + static_cast<char>(digit);
|
|
||||||
}
|
|
||||||
for (--place; place >= 0; --place) {
|
|
||||||
*dest++ = temp[place];
|
|
||||||
}
|
|
||||||
*dest = 0;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// it's ever so slightly faster to not have to strlen the key
|
|
||||||
template<typename T>
|
|
||||||
void WriteKey(JsonWriter &w, T &k) {
|
|
||||||
w.Key(k, sizeof(T) - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
struct WriteObject {
|
|
||||||
JsonWriter &writer;
|
|
||||||
WriteObject(JsonWriter &w)
|
|
||||||
: writer(w) {
|
|
||||||
writer.StartObject();
|
|
||||||
}
|
|
||||||
template<typename T>
|
|
||||||
WriteObject(JsonWriter &w, T &name)
|
|
||||||
: writer(w) {
|
|
||||||
WriteKey(writer, name);
|
|
||||||
writer.StartObject();
|
|
||||||
}
|
|
||||||
~WriteObject() { writer.EndObject(); }
|
|
||||||
};
|
|
||||||
|
|
||||||
struct WriteArray {
|
|
||||||
JsonWriter &writer;
|
|
||||||
template<typename T>
|
|
||||||
WriteArray(JsonWriter &w, T &name)
|
|
||||||
: writer(w) {
|
|
||||||
WriteKey(writer, name);
|
|
||||||
writer.StartArray();
|
|
||||||
}
|
|
||||||
~WriteArray() { writer.EndArray(); }
|
|
||||||
};
|
|
||||||
|
|
||||||
template<typename T>
|
|
||||||
void WriteOptionalString(JsonWriter &w, T &k, const char *value) {
|
|
||||||
|
|
||||||
if (value && value[0]) {
|
|
||||||
w.Key(k, sizeof(T) - 1);
|
|
||||||
w.String(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
static void JsonWriteNonce(JsonWriter &writer, const int nonce) {
|
|
||||||
|
|
||||||
WriteKey(writer, "nonce");
|
|
||||||
char nonceBuffer[32];
|
|
||||||
NumberToString(nonceBuffer, nonce);
|
|
||||||
writer.String(nonceBuffer);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t JsonWriteRichPresenceObj(char *dest, const size_t maxLen, const int nonce, const int pid, const DiscordRichPresence *presence) {
|
|
||||||
|
|
||||||
JsonWriter writer(dest, maxLen);
|
|
||||||
|
|
||||||
{
|
|
||||||
WriteObject top(writer);
|
|
||||||
|
|
||||||
JsonWriteNonce(writer, nonce);
|
|
||||||
|
|
||||||
WriteKey(writer, "cmd");
|
|
||||||
writer.String("SET_ACTIVITY");
|
|
||||||
|
|
||||||
{
|
|
||||||
WriteObject args(writer, "args");
|
|
||||||
|
|
||||||
WriteKey(writer, "pid");
|
|
||||||
writer.Int(pid);
|
|
||||||
|
|
||||||
if (presence != nullptr) {
|
|
||||||
WriteObject activity(writer, "activity");
|
|
||||||
|
|
||||||
if (presence->type >= 0 && presence->type <= 5) {
|
|
||||||
WriteKey(writer, "type");
|
|
||||||
writer.Int(presence->type);
|
|
||||||
|
|
||||||
WriteKey(writer, "status_display_type");
|
|
||||||
writer.Int(presence->status_display_type);
|
|
||||||
}
|
|
||||||
|
|
||||||
WriteOptionalString(writer, "name", presence->name);
|
|
||||||
WriteOptionalString(writer, "state", presence->state);
|
|
||||||
WriteOptionalString(writer, "details", presence->details);
|
|
||||||
|
|
||||||
if (presence->startTimestamp || presence->endTimestamp) {
|
|
||||||
WriteObject timestamps(writer, "timestamps");
|
|
||||||
|
|
||||||
if (presence->startTimestamp) {
|
|
||||||
WriteKey(writer, "start");
|
|
||||||
writer.Int64(presence->startTimestamp);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (presence->endTimestamp) {
|
|
||||||
WriteKey(writer, "end");
|
|
||||||
writer.Int64(presence->endTimestamp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((presence->largeImageKey && presence->largeImageKey[0]) ||
|
|
||||||
(presence->largeImageText && presence->largeImageText[0]) ||
|
|
||||||
(presence->smallImageKey && presence->smallImageKey[0]) ||
|
|
||||||
(presence->smallImageText && presence->smallImageText[0])) {
|
|
||||||
WriteObject assets(writer, "assets");
|
|
||||||
WriteOptionalString(writer, "large_image", presence->largeImageKey);
|
|
||||||
WriteOptionalString(writer, "large_text", presence->largeImageText);
|
|
||||||
WriteOptionalString(writer, "small_image", presence->smallImageKey);
|
|
||||||
WriteOptionalString(writer, "small_text", presence->smallImageText);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((presence->partyId && presence->partyId[0]) || presence->partySize ||
|
|
||||||
presence->partyMax || presence->partyPrivacy) {
|
|
||||||
WriteObject party(writer, "party");
|
|
||||||
WriteOptionalString(writer, "id", presence->partyId);
|
|
||||||
if (presence->partySize && presence->partyMax) {
|
|
||||||
WriteArray size(writer, "size");
|
|
||||||
writer.Int(presence->partySize);
|
|
||||||
writer.Int(presence->partyMax);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (presence->partyPrivacy) {
|
|
||||||
WriteKey(writer, "privacy");
|
|
||||||
writer.Int(presence->partyPrivacy);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((presence->matchSecret && presence->matchSecret[0]) ||
|
|
||||||
(presence->joinSecret && presence->joinSecret[0]) ||
|
|
||||||
(presence->spectateSecret && presence->spectateSecret[0])) {
|
|
||||||
WriteObject secrets(writer, "secrets");
|
|
||||||
WriteOptionalString(writer, "match", presence->matchSecret);
|
|
||||||
WriteOptionalString(writer, "join", presence->joinSecret);
|
|
||||||
WriteOptionalString(writer, "spectate", presence->spectateSecret);
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.Key("instance");
|
|
||||||
writer.Bool(presence->instance != 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return writer.Size();
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t JsonWriteHandshakeObj(char *dest, size_t maxLen, int version, const char *applicationId) {
|
|
||||||
|
|
||||||
JsonWriter writer(dest, maxLen);
|
|
||||||
|
|
||||||
{
|
|
||||||
WriteObject obj(writer);
|
|
||||||
WriteKey(writer, "v");
|
|
||||||
writer.Int(version);
|
|
||||||
WriteKey(writer, "client_id");
|
|
||||||
writer.String(applicationId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return writer.Size();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t JsonWriteSubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName) {
|
|
||||||
|
|
||||||
JsonWriter writer(dest, maxLen);
|
|
||||||
|
|
||||||
{
|
|
||||||
WriteObject obj(writer);
|
|
||||||
|
|
||||||
JsonWriteNonce(writer, nonce);
|
|
||||||
|
|
||||||
WriteKey(writer, "cmd");
|
|
||||||
writer.String("SUBSCRIBE");
|
|
||||||
|
|
||||||
WriteKey(writer, "evt");
|
|
||||||
writer.String(evtName);
|
|
||||||
}
|
|
||||||
|
|
||||||
return writer.Size();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t JsonWriteUnsubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName) {
|
|
||||||
|
|
||||||
JsonWriter writer(dest, maxLen);
|
|
||||||
|
|
||||||
{
|
|
||||||
WriteObject obj(writer);
|
|
||||||
|
|
||||||
JsonWriteNonce(writer, nonce);
|
|
||||||
|
|
||||||
WriteKey(writer, "cmd");
|
|
||||||
writer.String("UNSUBSCRIBE");
|
|
||||||
|
|
||||||
WriteKey(writer, "evt");
|
|
||||||
writer.String(evtName);
|
|
||||||
}
|
|
||||||
|
|
||||||
return writer.Size();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t JsonWriteJoinReply(char *dest, size_t maxLen, const char *userId, const int reply, const int nonce) {
|
|
||||||
|
|
||||||
JsonWriter writer(dest, maxLen);
|
|
||||||
|
|
||||||
{
|
|
||||||
WriteObject obj(writer);
|
|
||||||
|
|
||||||
WriteKey(writer, "cmd");
|
|
||||||
if (reply == DISCORD_REPLY_YES) {
|
|
||||||
writer.String("SEND_ACTIVITY_JOIN_INVITE");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
writer.String("CLOSE_ACTIVITY_JOIN_REQUEST");
|
|
||||||
}
|
|
||||||
|
|
||||||
WriteKey(writer, "args");
|
|
||||||
{
|
|
||||||
WriteObject args(writer);
|
|
||||||
|
|
||||||
WriteKey(writer, "user_id");
|
|
||||||
writer.String(userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
JsonWriteNonce(writer, nonce);
|
|
||||||
}
|
|
||||||
|
|
||||||
return writer.Size();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace discord_rpc
|
|
||||||
213
3rdparty/discord-rpc/discord_serialization.h
vendored
213
3rdparty/discord-rpc/discord_serialization.h
vendored
@@ -1,213 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2017 Discord, Inc.
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
* this software and associated documentation files (the "Software"), to deal in
|
|
||||||
* the Software without restriction, including without limitation the rights to
|
|
||||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
||||||
* of the Software, and to permit persons to whom the Software is furnished to do
|
|
||||||
* so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef DISCORD_SERIALIZATION_H
|
|
||||||
#define DISCORD_SERIALIZATION_H
|
|
||||||
|
|
||||||
#include <rapidjson/document.h>
|
|
||||||
#include <rapidjson/stringbuffer.h>
|
|
||||||
#include <rapidjson/writer.h>
|
|
||||||
|
|
||||||
struct DiscordRichPresence;
|
|
||||||
|
|
||||||
namespace discord_rpc {
|
|
||||||
|
|
||||||
// if only there was a standard library function for this
|
|
||||||
template<size_t Len>
|
|
||||||
inline size_t StringCopy(char (&dest)[Len], const char *src) {
|
|
||||||
if (!src || !Len) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
size_t copied;
|
|
||||||
char *out = dest;
|
|
||||||
for (copied = 1; *src && copied < Len; ++copied) {
|
|
||||||
*out++ = *src++;
|
|
||||||
}
|
|
||||||
*out = 0;
|
|
||||||
return copied - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t JsonWriteHandshakeObj(char *dest, size_t maxLen, int version, const char *applicationId);
|
|
||||||
|
|
||||||
// Commands
|
|
||||||
size_t JsonWriteRichPresenceObj(char *dest, const size_t maxLen, const int nonce, const int pid, const DiscordRichPresence *presence);
|
|
||||||
size_t JsonWriteSubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName);
|
|
||||||
|
|
||||||
size_t JsonWriteUnsubscribeCommand(char *dest, size_t maxLen, int nonce, const char *evtName);
|
|
||||||
|
|
||||||
size_t JsonWriteJoinReply(char *dest, size_t maxLen, const char *userId, int reply, int nonce);
|
|
||||||
|
|
||||||
// I want to use as few allocations as I can get away with, and to do that with RapidJson, you need
|
|
||||||
// to supply some of your own allocators for stuff rather than use the defaults
|
|
||||||
|
|
||||||
class LinearAllocator {
|
|
||||||
public:
|
|
||||||
char *buffer_;
|
|
||||||
char *end_;
|
|
||||||
LinearAllocator() {
|
|
||||||
assert(0); // needed for some default case in rapidjson, should not use
|
|
||||||
}
|
|
||||||
LinearAllocator(char *buffer, size_t size)
|
|
||||||
: buffer_(buffer), end_(buffer + size) {
|
|
||||||
}
|
|
||||||
static const bool kNeedFree = false;
|
|
||||||
void *Malloc(size_t size) {
|
|
||||||
char *res = buffer_;
|
|
||||||
buffer_ += size;
|
|
||||||
if (buffer_ > end_) {
|
|
||||||
buffer_ = res;
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
void *Realloc(void *originalPtr, size_t originalSize, size_t newSize) {
|
|
||||||
if (newSize == 0) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
// allocate how much you need in the first place
|
|
||||||
assert(!originalPtr && !originalSize);
|
|
||||||
// unused parameter warning
|
|
||||||
(void)(originalPtr);
|
|
||||||
(void)(originalSize);
|
|
||||||
return Malloc(newSize);
|
|
||||||
}
|
|
||||||
static void Free(void *ptr) {
|
|
||||||
/* shrug */
|
|
||||||
(void)ptr;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
template<size_t Size>
|
|
||||||
class FixedLinearAllocator : public LinearAllocator {
|
|
||||||
public:
|
|
||||||
char fixedBuffer_[Size];
|
|
||||||
FixedLinearAllocator()
|
|
||||||
: LinearAllocator(fixedBuffer_, Size) {
|
|
||||||
}
|
|
||||||
static const bool kNeedFree = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// wonder why this isn't a thing already, maybe I missed it
|
|
||||||
class DirectStringBuffer {
|
|
||||||
public:
|
|
||||||
using Ch = char;
|
|
||||||
char *buffer_;
|
|
||||||
char *end_;
|
|
||||||
char *current_;
|
|
||||||
|
|
||||||
DirectStringBuffer(char *buffer, size_t maxLen)
|
|
||||||
: buffer_(buffer), end_(buffer + maxLen), current_(buffer) {
|
|
||||||
}
|
|
||||||
|
|
||||||
void Put(char c) {
|
|
||||||
if (current_ < end_) {
|
|
||||||
*current_++ = c;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
void Flush() {}
|
|
||||||
size_t GetSize() const { return static_cast<size_t>(current_ - buffer_); }
|
|
||||||
};
|
|
||||||
|
|
||||||
using MallocAllocator = rapidjson::CrtAllocator;
|
|
||||||
using PoolAllocator = rapidjson::MemoryPoolAllocator<MallocAllocator>;
|
|
||||||
using UTF8 = rapidjson::UTF8<char>;
|
|
||||||
// Writer appears to need about 16 bytes per nested object level (with 64bit size_t)
|
|
||||||
using StackAllocator = FixedLinearAllocator<2048>;
|
|
||||||
constexpr size_t WriterNestingLevels = 2048 / (2 * sizeof(size_t));
|
|
||||||
using JsonWriterBase =
|
|
||||||
rapidjson::Writer<DirectStringBuffer, UTF8, UTF8, StackAllocator, rapidjson::kWriteNoFlags>;
|
|
||||||
class JsonWriter : public JsonWriterBase {
|
|
||||||
public:
|
|
||||||
DirectStringBuffer stringBuffer_;
|
|
||||||
StackAllocator stackAlloc_;
|
|
||||||
|
|
||||||
JsonWriter(char *dest, size_t maxLen)
|
|
||||||
: JsonWriterBase(stringBuffer_, &stackAlloc_, WriterNestingLevels), stringBuffer_(dest, maxLen), stackAlloc_() {
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t Size() const { return stringBuffer_.GetSize(); }
|
|
||||||
};
|
|
||||||
|
|
||||||
using JsonDocumentBase = rapidjson::GenericDocument<UTF8, PoolAllocator, StackAllocator>;
|
|
||||||
class JsonDocument : public JsonDocumentBase {
|
|
||||||
public:
|
|
||||||
static const int kDefaultChunkCapacity = 32 * 1024;
|
|
||||||
// json parser will use this buffer first, then allocate more if needed; I seriously doubt we
|
|
||||||
// send any messages that would use all of this, though.
|
|
||||||
char parseBuffer_[32 * 1024];
|
|
||||||
MallocAllocator mallocAllocator_;
|
|
||||||
PoolAllocator poolAllocator_;
|
|
||||||
StackAllocator stackAllocator_;
|
|
||||||
JsonDocument()
|
|
||||||
: JsonDocumentBase(rapidjson::kObjectType,
|
|
||||||
&poolAllocator_,
|
|
||||||
sizeof(stackAllocator_.fixedBuffer_),
|
|
||||||
&stackAllocator_),
|
|
||||||
poolAllocator_(parseBuffer_, sizeof(parseBuffer_), kDefaultChunkCapacity, &mallocAllocator_), stackAllocator_() {
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
using JsonValue = rapidjson::GenericValue<UTF8, PoolAllocator>;
|
|
||||||
|
|
||||||
inline JsonValue *GetObjMember(JsonValue *obj, const char *name) {
|
|
||||||
|
|
||||||
if (obj) {
|
|
||||||
auto member = obj->FindMember(name);
|
|
||||||
if (member != obj->MemberEnd() && member->value.IsObject()) {
|
|
||||||
return &member->value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nullptr;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
inline int GetIntMember(JsonValue *obj, const char *name, int notFoundDefault = 0) {
|
|
||||||
|
|
||||||
if (obj) {
|
|
||||||
auto member = obj->FindMember(name);
|
|
||||||
if (member != obj->MemberEnd() && member->value.IsInt()) {
|
|
||||||
return member->value.GetInt();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return notFoundDefault;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
inline const char *GetStrMember(JsonValue *obj, const char *name, const char *notFoundDefault = nullptr) {
|
|
||||||
|
|
||||||
if (obj) {
|
|
||||||
auto member = obj->FindMember(name);
|
|
||||||
if (member != obj->MemberEnd() && member->value.IsString()) {
|
|
||||||
return member->value.GetString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return notFoundDefault;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace discord_rpc
|
|
||||||
|
|
||||||
#endif // DISCORD_SERIALIZATION_H
|
|
||||||
3
3rdparty/getopt/CMakeLists.txt
vendored
Normal file
3
3rdparty/getopt/CMakeLists.txt
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
add_library(getopt STATIC getopt.cpp)
|
||||||
|
target_compile_definitions(getopt PRIVATE -DSTATIC_GETOPT -D_UNICODE)
|
||||||
|
target_include_directories(getopt PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
753
3rdparty/getopt/getopt.cpp
vendored
Normal file
753
3rdparty/getopt/getopt.cpp
vendored
Normal file
@@ -0,0 +1,753 @@
|
|||||||
|
/* Getopt for Microsoft C
|
||||||
|
This code is a modification of the Free Software Foundation, Inc.
|
||||||
|
Getopt library for parsing command line argument the purpose was
|
||||||
|
to provide a Microsoft Visual C friendly derivative. This code
|
||||||
|
provides functionality for both Unicode and Multibyte builds.
|
||||||
|
|
||||||
|
Date: 02/03/2011 - Ludvik Jerabek - Initial Release
|
||||||
|
Version: 1.1
|
||||||
|
Comment: Supports getopt, getopt_long, and getopt_long_only
|
||||||
|
and POSIXLY_CORRECT environment flag
|
||||||
|
License: LGPL
|
||||||
|
|
||||||
|
Revisions:
|
||||||
|
|
||||||
|
02/03/2011 - Ludvik Jerabek - Initial Release
|
||||||
|
02/20/2011 - Ludvik Jerabek - Fixed compiler warnings at Level 4
|
||||||
|
07/05/2011 - Ludvik Jerabek - Added no_argument, required_argument, optional_argument defs
|
||||||
|
08/03/2011 - Ludvik Jerabek - Fixed non-argument runtime bug which caused runtime exception
|
||||||
|
08/09/2011 - Ludvik Jerabek - Added code to export functions for DLL and LIB
|
||||||
|
02/15/2012 - Ludvik Jerabek - Fixed _GETOPT_THROW definition missing in implementation file
|
||||||
|
08/01/2012 - Ludvik Jerabek - Created separate functions for char and wchar_t characters so single dll can do both unicode and ansi
|
||||||
|
10/15/2012 - Ludvik Jerabek - Modified to match latest GNU features
|
||||||
|
06/19/2015 - Ludvik Jerabek - Fixed maximum option limitation caused by option_a (255) and option_w (65535) structure val variable
|
||||||
|
09/24/2022 - Ludvik Jerabek - Updated to match most recent getopt release
|
||||||
|
09/25/2022 - Ludvik Jerabek - Fixed memory allocation (malloc call) issue for wchar_t*
|
||||||
|
|
||||||
|
**DISCLAIMER**
|
||||||
|
THIS MATERIAL IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EITHER EXPRESS OR IMPLIED, INCLUDING, BUT Not LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE, OR NON-INFRINGEMENT. SOME JURISDICTIONS DO NOT ALLOW THE
|
||||||
|
EXCLUSION OF IMPLIED WARRANTIES, SO THE ABOVE EXCLUSION MAY NOT
|
||||||
|
APPLY TO YOU. IN NO EVENT WILL I BE LIABLE TO ANY PARTY FOR ANY
|
||||||
|
DIRECT, INDIRECT, SPECIAL OR OTHER CONSEQUENTIAL DAMAGES FOR ANY
|
||||||
|
USE OF THIS MATERIAL INCLUDING, WITHOUT LIMITATION, ANY LOST
|
||||||
|
PROFITS, BUSINESS INTERRUPTION, LOSS OF PROGRAMS OR OTHER DATA ON
|
||||||
|
YOUR INFORMATION HANDLING SYSTEM OR OTHERWISE, EVEN If WE ARE
|
||||||
|
EXPRESSLY ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
||||||
|
*/
|
||||||
|
#define _CRT_SECURE_NO_WARNINGS
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <malloc.h>
|
||||||
|
#include "getopt.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
# define _GETOPT_THROW throw()
|
||||||
|
#else
|
||||||
|
# define _GETOPT_THROW
|
||||||
|
#endif
|
||||||
|
|
||||||
|
int optind = 1;
|
||||||
|
int opterr = 1;
|
||||||
|
int optopt = '?';
|
||||||
|
enum ENUM_ORDERING {
|
||||||
|
REQUIRE_ORDER,
|
||||||
|
PERMUTE,
|
||||||
|
RETURN_IN_ORDER
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Ansi structures and functions follow
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
|
static struct _getopt_data_a {
|
||||||
|
int optind;
|
||||||
|
int opterr;
|
||||||
|
int optopt;
|
||||||
|
char *optarg;
|
||||||
|
int __initialized;
|
||||||
|
char *__nextchar;
|
||||||
|
enum ENUM_ORDERING __ordering;
|
||||||
|
int __first_nonopt;
|
||||||
|
int __last_nonopt;
|
||||||
|
} getopt_data_a;
|
||||||
|
char *optarg_a;
|
||||||
|
|
||||||
|
static void exchange_a(char **argv, struct _getopt_data_a *d) {
|
||||||
|
int bottom = d->__first_nonopt;
|
||||||
|
int middle = d->__last_nonopt;
|
||||||
|
int top = d->optind;
|
||||||
|
char *tem;
|
||||||
|
while (top > middle && middle > bottom) {
|
||||||
|
if (top - middle > middle - bottom) {
|
||||||
|
int len = middle - bottom;
|
||||||
|
int i;
|
||||||
|
for (i = 0; i < len; i++) {
|
||||||
|
tem = argv[bottom + i];
|
||||||
|
argv[bottom + i] = argv[top - (middle - bottom) + i];
|
||||||
|
argv[top - (middle - bottom) + i] = tem;
|
||||||
|
}
|
||||||
|
top -= len;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
int len = top - middle;
|
||||||
|
int i;
|
||||||
|
for (i = 0; i < len; i++) {
|
||||||
|
tem = argv[bottom + i];
|
||||||
|
argv[bottom + i] = argv[middle + i];
|
||||||
|
argv[middle + i] = tem;
|
||||||
|
}
|
||||||
|
bottom += len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d->__first_nonopt += (d->optind - d->__last_nonopt);
|
||||||
|
d->__last_nonopt = d->optind;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int process_long_option_a(int argc, char **argv, const char *optstring, const struct option_a *longopts, int *longind, int long_only, struct _getopt_data_a *d, int print_errors, const char *prefix);
|
||||||
|
static int process_long_option_a(int argc, char **argv, const char *optstring, const struct option_a *longopts, int *longind, int long_only, struct _getopt_data_a *d, int print_errors, const char *prefix) {
|
||||||
|
assert(longopts != NULL);
|
||||||
|
char *nameend;
|
||||||
|
size_t namelen;
|
||||||
|
const struct option_a *p;
|
||||||
|
const struct option_a *pfound = NULL;
|
||||||
|
int n_options;
|
||||||
|
int option_index = 0;
|
||||||
|
for (nameend = d->__nextchar; *nameend && *nameend != '='; nameend++)
|
||||||
|
;
|
||||||
|
namelen = nameend - d->__nextchar;
|
||||||
|
for (p = longopts, n_options = 0; p->name; p++, n_options++)
|
||||||
|
if (!strncmp(p->name, d->__nextchar, namelen) && namelen == strlen(p->name)) {
|
||||||
|
pfound = p;
|
||||||
|
option_index = n_options;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (pfound == NULL) {
|
||||||
|
unsigned char *ambig_set = NULL;
|
||||||
|
int ambig_fallback = 0;
|
||||||
|
int indfound = -1;
|
||||||
|
for (p = longopts, option_index = 0; p->name; p++, option_index++)
|
||||||
|
if (!strncmp(p->name, d->__nextchar, namelen)) {
|
||||||
|
if (pfound == NULL) {
|
||||||
|
pfound = p;
|
||||||
|
indfound = option_index;
|
||||||
|
}
|
||||||
|
else if (long_only || pfound->has_arg != p->has_arg || pfound->flag != p->flag || pfound->val != p->val) {
|
||||||
|
if (!ambig_fallback) {
|
||||||
|
if (!print_errors)
|
||||||
|
ambig_fallback = 1;
|
||||||
|
|
||||||
|
else if (!ambig_set) {
|
||||||
|
if ((ambig_set = reinterpret_cast<unsigned char *>(malloc(n_options * sizeof(char)))) == NULL)
|
||||||
|
ambig_fallback = 1;
|
||||||
|
|
||||||
|
if (ambig_set) {
|
||||||
|
memset(ambig_set, 0, n_options * sizeof(char));
|
||||||
|
ambig_set[indfound] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ambig_set)
|
||||||
|
ambig_set[option_index] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ambig_set || ambig_fallback) {
|
||||||
|
if (print_errors) {
|
||||||
|
if (ambig_fallback)
|
||||||
|
fprintf(stderr, "%s: option '%s%s' is ambiguous\n", argv[0], prefix, d->__nextchar);
|
||||||
|
else {
|
||||||
|
_lock_file(stderr);
|
||||||
|
fprintf(stderr, "%s: option '%s%s' is ambiguous; possibilities:", argv[0], prefix, d->__nextchar);
|
||||||
|
for (option_index = 0; option_index < n_options; option_index++)
|
||||||
|
if (ambig_set[option_index])
|
||||||
|
fprintf(stderr, " '%s%s'", prefix, longopts[option_index].name);
|
||||||
|
fprintf(stderr, "\n");
|
||||||
|
_unlock_file(stderr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
free(ambig_set);
|
||||||
|
d->__nextchar += strlen(d->__nextchar);
|
||||||
|
d->optind++;
|
||||||
|
d->optopt = 0;
|
||||||
|
return '?';
|
||||||
|
}
|
||||||
|
option_index = indfound;
|
||||||
|
}
|
||||||
|
if (pfound == NULL) {
|
||||||
|
if (!long_only || argv[d->optind][1] == '-' || strchr(optstring, *d->__nextchar) == NULL) {
|
||||||
|
if (print_errors)
|
||||||
|
fprintf(stderr, "%s: unrecognized option '%s%s'\n", argv[0], prefix, d->__nextchar);
|
||||||
|
d->__nextchar = NULL;
|
||||||
|
d->optind++;
|
||||||
|
d->optopt = 0;
|
||||||
|
return '?';
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
d->optind++;
|
||||||
|
d->__nextchar = NULL;
|
||||||
|
if (*nameend) {
|
||||||
|
if (pfound->has_arg)
|
||||||
|
d->optarg = nameend + 1;
|
||||||
|
else {
|
||||||
|
if (print_errors)
|
||||||
|
fprintf(stderr, "%s: option '%s%s' doesn't allow an argument\n", argv[0], prefix, pfound->name);
|
||||||
|
d->optopt = pfound->val;
|
||||||
|
return '?';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (pfound->has_arg == 1) {
|
||||||
|
if (d->optind < argc)
|
||||||
|
d->optarg = argv[d->optind++];
|
||||||
|
else {
|
||||||
|
if (print_errors)
|
||||||
|
fprintf(stderr, "%s: option '%s%s' requires an argument\n", argv[0], prefix, pfound->name);
|
||||||
|
d->optopt = pfound->val;
|
||||||
|
return optstring[0] == ':' ? ':' : '?';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (longind != NULL)
|
||||||
|
*longind = option_index;
|
||||||
|
|
||||||
|
if (pfound->flag) {
|
||||||
|
*(pfound->flag) = pfound->val;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return pfound->val;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *_getopt_initialize_a(const char *optstring, struct _getopt_data_a *d, int posixly_correct) {
|
||||||
|
if (d->optind == 0)
|
||||||
|
d->optind = 1;
|
||||||
|
|
||||||
|
d->__first_nonopt = d->__last_nonopt = d->optind;
|
||||||
|
d->__nextchar = NULL;
|
||||||
|
|
||||||
|
if (optstring[0] == '-') {
|
||||||
|
d->__ordering = RETURN_IN_ORDER;
|
||||||
|
++optstring;
|
||||||
|
}
|
||||||
|
else if (optstring[0] == '+') {
|
||||||
|
d->__ordering = REQUIRE_ORDER;
|
||||||
|
++optstring;
|
||||||
|
}
|
||||||
|
else if (posixly_correct | !!getenv("POSIXLY_CORRECT"))
|
||||||
|
d->__ordering = REQUIRE_ORDER;
|
||||||
|
else
|
||||||
|
d->__ordering = PERMUTE;
|
||||||
|
|
||||||
|
d->__initialized = 1;
|
||||||
|
return optstring;
|
||||||
|
}
|
||||||
|
|
||||||
|
int _getopt_internal_r_a(int argc, char *const *argv, const char *optstring, const struct option_a *longopts, int *longind, int long_only, struct _getopt_data_a *d, int posixly_correct);
|
||||||
|
int _getopt_internal_r_a(int argc, char *const *argv, const char *optstring, const struct option_a *longopts, int *longind, int long_only, struct _getopt_data_a *d, int posixly_correct) {
|
||||||
|
int print_errors = d->opterr;
|
||||||
|
if (argc < 1)
|
||||||
|
return -1;
|
||||||
|
d->optarg = NULL;
|
||||||
|
if (d->optind == 0 || !d->__initialized)
|
||||||
|
optstring = _getopt_initialize_a(optstring, d, posixly_correct);
|
||||||
|
else if (optstring[0] == '-' || optstring[0] == '+')
|
||||||
|
optstring++;
|
||||||
|
if (optstring[0] == ':')
|
||||||
|
print_errors = 0;
|
||||||
|
|
||||||
|
if (d->__nextchar == NULL || *d->__nextchar == '\0') {
|
||||||
|
if (d->__last_nonopt > d->optind)
|
||||||
|
d->__last_nonopt = d->optind;
|
||||||
|
if (d->__first_nonopt > d->optind)
|
||||||
|
d->__first_nonopt = d->optind;
|
||||||
|
if (d->__ordering == PERMUTE) {
|
||||||
|
if (d->__first_nonopt != d->__last_nonopt && d->__last_nonopt != d->optind)
|
||||||
|
exchange_a(const_cast<char **>(argv), d);
|
||||||
|
else if (d->__last_nonopt != d->optind)
|
||||||
|
d->__first_nonopt = d->optind;
|
||||||
|
while (d->optind < argc && (argv[d->optind][0] != '-' || argv[d->optind][1] == '\0'))
|
||||||
|
d->optind++;
|
||||||
|
d->__last_nonopt = d->optind;
|
||||||
|
}
|
||||||
|
if (d->optind != argc && !strcmp(argv[d->optind], "--")) {
|
||||||
|
d->optind++;
|
||||||
|
if (d->__first_nonopt != d->__last_nonopt && d->__last_nonopt != d->optind)
|
||||||
|
exchange_a(const_cast<char **>(argv), d);
|
||||||
|
else if (d->__first_nonopt == d->__last_nonopt)
|
||||||
|
d->__first_nonopt = d->optind;
|
||||||
|
d->__last_nonopt = argc;
|
||||||
|
d->optind = argc;
|
||||||
|
}
|
||||||
|
if (d->optind == argc) {
|
||||||
|
if (d->__first_nonopt != d->__last_nonopt)
|
||||||
|
d->optind = d->__first_nonopt;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (argv[d->optind][0] != '-' || argv[d->optind][1] == '\0') {
|
||||||
|
if (d->__ordering == REQUIRE_ORDER)
|
||||||
|
return -1;
|
||||||
|
d->optarg = argv[d->optind++];
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (longopts) {
|
||||||
|
if (argv[d->optind][1] == '-') {
|
||||||
|
d->__nextchar = argv[d->optind] + 2;
|
||||||
|
return process_long_option_a(argc, const_cast<char **>(argv), optstring, longopts, longind, long_only, d, print_errors, "--");
|
||||||
|
}
|
||||||
|
if (long_only && (argv[d->optind][2] || !strchr(optstring, argv[d->optind][1]))) {
|
||||||
|
int code;
|
||||||
|
d->__nextchar = argv[d->optind] + 1;
|
||||||
|
code = process_long_option_a(argc, const_cast<char **>(argv), optstring, longopts,
|
||||||
|
longind, long_only, d,
|
||||||
|
print_errors, "-");
|
||||||
|
if (code != -1)
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d->__nextchar = argv[d->optind] + 1;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
char c = *d->__nextchar++;
|
||||||
|
const char *temp = strchr(optstring, c);
|
||||||
|
if (*d->__nextchar == '\0')
|
||||||
|
++d->optind;
|
||||||
|
if (temp == NULL || c == ':' || c == ';') {
|
||||||
|
if (print_errors)
|
||||||
|
fprintf(stderr, "%s: invalid option -- '%c'\n", argv[0], c);
|
||||||
|
d->optopt = c;
|
||||||
|
return '?';
|
||||||
|
}
|
||||||
|
if (temp[0] == 'W' && temp[1] == ';' && longopts != NULL) {
|
||||||
|
if (*d->__nextchar != '\0')
|
||||||
|
d->optarg = d->__nextchar;
|
||||||
|
else if (d->optind == argc) {
|
||||||
|
if (print_errors)
|
||||||
|
fprintf(stderr, "%s: option requires an argument -- '%c'\n", argv[0], c);
|
||||||
|
d->optopt = c;
|
||||||
|
if (optstring[0] == ':')
|
||||||
|
c = ':';
|
||||||
|
else
|
||||||
|
c = '?';
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
d->optarg = argv[d->optind];
|
||||||
|
d->__nextchar = d->optarg;
|
||||||
|
d->optarg = NULL;
|
||||||
|
return process_long_option_a(argc, const_cast<char **>(argv), optstring, longopts, longind, 0, d, print_errors, "-W ");
|
||||||
|
}
|
||||||
|
if (temp[1] == ':') {
|
||||||
|
if (temp[2] == ':') {
|
||||||
|
if (*d->__nextchar != '\0') {
|
||||||
|
d->optarg = d->__nextchar;
|
||||||
|
d->optind++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
d->optarg = NULL;
|
||||||
|
d->__nextchar = NULL;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (*d->__nextchar != '\0') {
|
||||||
|
d->optarg = d->__nextchar;
|
||||||
|
d->optind++;
|
||||||
|
}
|
||||||
|
else if (d->optind == argc) {
|
||||||
|
if (print_errors)
|
||||||
|
fprintf(stderr, "%s: option requires an argument -- '%c'\n", argv[0], c);
|
||||||
|
d->optopt = c;
|
||||||
|
if (optstring[0] == ':')
|
||||||
|
c = ':';
|
||||||
|
else
|
||||||
|
c = '?';
|
||||||
|
}
|
||||||
|
else
|
||||||
|
d->optarg = argv[d->optind++];
|
||||||
|
d->__nextchar = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int _getopt_internal_a(int argc, char *const *argv, const char *optstring, const struct option_a *longopts, int *longind, int long_only, int posixly_correct);
|
||||||
|
int _getopt_internal_a(int argc, char *const *argv, const char *optstring, const struct option_a *longopts, int *longind, int long_only, int posixly_correct) {
|
||||||
|
int result;
|
||||||
|
getopt_data_a.optind = optind;
|
||||||
|
getopt_data_a.opterr = opterr;
|
||||||
|
result = _getopt_internal_r_a(argc, argv, optstring, longopts, longind, long_only, &getopt_data_a, posixly_correct);
|
||||||
|
optind = getopt_data_a.optind;
|
||||||
|
optarg_a = getopt_data_a.optarg;
|
||||||
|
optopt = getopt_data_a.optopt;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
int getopt_a(int argc, char *const *argv, const char *optstring) _GETOPT_THROW {
|
||||||
|
return _getopt_internal_a(argc, argv, optstring, static_cast<const struct option_a *>(0), static_cast<int *>(0), 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int getopt_long_a(int argc, char *const *argv, const char *options, const struct option_a *long_options, int *opt_index) _GETOPT_THROW {
|
||||||
|
return _getopt_internal_a(argc, argv, options, long_options, opt_index, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int getopt_long_only_a(int argc, char *const *argv, const char *options, const struct option_a *long_options, int *opt_index) _GETOPT_THROW {
|
||||||
|
return _getopt_internal_a(argc, argv, options, long_options, opt_index, 1, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int _getopt_long_r_a(int argc, char *const *argv, const char *options, const struct option_a *long_options, int *opt_index, struct _getopt_data_a *d);
|
||||||
|
int _getopt_long_r_a(int argc, char *const *argv, const char *options, const struct option_a *long_options, int *opt_index, struct _getopt_data_a *d) {
|
||||||
|
return _getopt_internal_r_a(argc, argv, options, long_options, opt_index, 0, d, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int _getopt_long_only_r_a(int argc, char *const *argv, const char *options, const struct option_a *long_options, int *opt_index, struct _getopt_data_a *d);
|
||||||
|
int _getopt_long_only_r_a(int argc, char *const *argv, const char *options, const struct option_a *long_options, int *opt_index, struct _getopt_data_a *d) {
|
||||||
|
return _getopt_internal_r_a(argc, argv, options, long_options, opt_index, 1, d, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Unicode Structures and Functions
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
|
static struct _getopt_data_w {
|
||||||
|
int optind;
|
||||||
|
int opterr;
|
||||||
|
int optopt;
|
||||||
|
wchar_t *optarg;
|
||||||
|
int __initialized;
|
||||||
|
wchar_t *__nextchar;
|
||||||
|
enum ENUM_ORDERING __ordering;
|
||||||
|
int __first_nonopt;
|
||||||
|
int __last_nonopt;
|
||||||
|
} getopt_data_w;
|
||||||
|
wchar_t *optarg_w;
|
||||||
|
|
||||||
|
static void exchange_w(wchar_t **argv, struct _getopt_data_w *d) {
|
||||||
|
int bottom = d->__first_nonopt;
|
||||||
|
int middle = d->__last_nonopt;
|
||||||
|
int top = d->optind;
|
||||||
|
wchar_t *tem;
|
||||||
|
while (top > middle && middle > bottom) {
|
||||||
|
if (top - middle > middle - bottom) {
|
||||||
|
int len = middle - bottom;
|
||||||
|
int i;
|
||||||
|
for (i = 0; i < len; i++) {
|
||||||
|
tem = argv[bottom + i];
|
||||||
|
argv[bottom + i] = argv[top - (middle - bottom) + i];
|
||||||
|
argv[top - (middle - bottom) + i] = tem;
|
||||||
|
}
|
||||||
|
top -= len;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
int len = top - middle;
|
||||||
|
int i;
|
||||||
|
for (i = 0; i < len; i++) {
|
||||||
|
tem = argv[bottom + i];
|
||||||
|
argv[bottom + i] = argv[middle + i];
|
||||||
|
argv[middle + i] = tem;
|
||||||
|
}
|
||||||
|
bottom += len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d->__first_nonopt += (d->optind - d->__last_nonopt);
|
||||||
|
d->__last_nonopt = d->optind;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int process_long_option_w(int argc, wchar_t **argv, const wchar_t *optstring, const struct option_w *longopts, int *longind, int long_only, struct _getopt_data_w *d, int print_errors, const wchar_t *prefix) {
|
||||||
|
assert(longopts != NULL);
|
||||||
|
wchar_t *nameend;
|
||||||
|
size_t namelen;
|
||||||
|
const struct option_w *p;
|
||||||
|
const struct option_w *pfound = NULL;
|
||||||
|
int n_options;
|
||||||
|
int option_index = 0;
|
||||||
|
for (nameend = d->__nextchar; *nameend && *nameend != L'='; nameend++)
|
||||||
|
;
|
||||||
|
namelen = nameend - d->__nextchar;
|
||||||
|
for (p = longopts, n_options = 0; p->name; p++, n_options++)
|
||||||
|
if (!wcsncmp(p->name, d->__nextchar, namelen) && namelen == wcslen(p->name)) {
|
||||||
|
pfound = p;
|
||||||
|
option_index = n_options;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (pfound == NULL) {
|
||||||
|
wchar_t *ambig_set = NULL;
|
||||||
|
int ambig_fallback = 0;
|
||||||
|
int indfound = -1;
|
||||||
|
for (p = longopts, option_index = 0; p->name; p++, option_index++)
|
||||||
|
if (!wcsncmp(p->name, d->__nextchar, namelen)) {
|
||||||
|
if (pfound == NULL) {
|
||||||
|
pfound = p;
|
||||||
|
indfound = option_index;
|
||||||
|
}
|
||||||
|
else if (long_only || pfound->has_arg != p->has_arg || pfound->flag != p->flag || pfound->val != p->val) {
|
||||||
|
if (!ambig_fallback) {
|
||||||
|
if (!print_errors)
|
||||||
|
ambig_fallback = 1;
|
||||||
|
|
||||||
|
else if (!ambig_set) {
|
||||||
|
if ((ambig_set = reinterpret_cast<wchar_t *>(malloc(n_options * sizeof(wchar_t)))) == NULL)
|
||||||
|
ambig_fallback = 1;
|
||||||
|
|
||||||
|
if (ambig_set) {
|
||||||
|
memset(ambig_set, 0, n_options * sizeof(wchar_t));
|
||||||
|
ambig_set[indfound] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ambig_set)
|
||||||
|
ambig_set[option_index] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ambig_set || ambig_fallback) {
|
||||||
|
if (print_errors) {
|
||||||
|
if (ambig_fallback)
|
||||||
|
fwprintf(stderr, L"%s: option '%s%s' is ambiguous\n", argv[0], prefix, d->__nextchar);
|
||||||
|
else {
|
||||||
|
_lock_file(stderr);
|
||||||
|
fwprintf(stderr, L"%s: option '%s%s' is ambiguous; possibilities:", argv[0], prefix, d->__nextchar);
|
||||||
|
for (option_index = 0; option_index < n_options; option_index++)
|
||||||
|
if (ambig_set[option_index])
|
||||||
|
fwprintf(stderr, L" '%s%s'", prefix, longopts[option_index].name);
|
||||||
|
fwprintf(stderr, L"\n");
|
||||||
|
_unlock_file(stderr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
free(ambig_set);
|
||||||
|
d->__nextchar += wcslen(d->__nextchar);
|
||||||
|
d->optind++;
|
||||||
|
d->optopt = 0;
|
||||||
|
return L'?';
|
||||||
|
}
|
||||||
|
option_index = indfound;
|
||||||
|
}
|
||||||
|
if (pfound == NULL) {
|
||||||
|
if (!long_only || argv[d->optind][1] == L'-' || wcschr(optstring, *d->__nextchar) == NULL) {
|
||||||
|
if (print_errors)
|
||||||
|
fwprintf(stderr, L"%s: unrecognized option '%s%s'\n", argv[0], prefix, d->__nextchar);
|
||||||
|
d->__nextchar = NULL;
|
||||||
|
d->optind++;
|
||||||
|
d->optopt = 0;
|
||||||
|
return L'?';
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
d->optind++;
|
||||||
|
d->__nextchar = NULL;
|
||||||
|
if (*nameend) {
|
||||||
|
if (pfound->has_arg)
|
||||||
|
d->optarg = nameend + 1;
|
||||||
|
else {
|
||||||
|
if (print_errors)
|
||||||
|
fwprintf(stderr, L"%s: option '%s%s' doesn't allow an argument\n", argv[0], prefix, pfound->name);
|
||||||
|
d->optopt = pfound->val;
|
||||||
|
return L'?';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (pfound->has_arg == 1) {
|
||||||
|
if (d->optind < argc)
|
||||||
|
d->optarg = argv[d->optind++];
|
||||||
|
else {
|
||||||
|
if (print_errors)
|
||||||
|
fwprintf(stderr, L"%s: option '%s%s' requires an argument\n", argv[0], prefix, pfound->name);
|
||||||
|
d->optopt = pfound->val;
|
||||||
|
return optstring[0] == L':' ? L':' : L'?';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (longind != NULL)
|
||||||
|
*longind = option_index;
|
||||||
|
if (pfound->flag) {
|
||||||
|
*(pfound->flag) = pfound->val;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return pfound->val;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const wchar_t *_getopt_initialize_w(const wchar_t *optstring, struct _getopt_data_w *d, int posixly_correct) {
|
||||||
|
if (d->optind == 0)
|
||||||
|
d->optind = 1;
|
||||||
|
|
||||||
|
d->__first_nonopt = d->__last_nonopt = d->optind;
|
||||||
|
d->__nextchar = NULL;
|
||||||
|
|
||||||
|
if (optstring[0] == L'-') {
|
||||||
|
d->__ordering = RETURN_IN_ORDER;
|
||||||
|
++optstring;
|
||||||
|
}
|
||||||
|
else if (optstring[0] == L'+') {
|
||||||
|
d->__ordering = REQUIRE_ORDER;
|
||||||
|
++optstring;
|
||||||
|
}
|
||||||
|
else if (posixly_correct | !!_wgetenv(L"POSIXLY_CORRECT"))
|
||||||
|
d->__ordering = REQUIRE_ORDER;
|
||||||
|
else
|
||||||
|
d->__ordering = PERMUTE;
|
||||||
|
|
||||||
|
d->__initialized = 1;
|
||||||
|
return optstring;
|
||||||
|
}
|
||||||
|
|
||||||
|
int _getopt_internal_r_w(int argc, wchar_t *const *argv, const wchar_t *optstring, const struct option_w *longopts, int *longind, int long_only, struct _getopt_data_w *d, int posixly_correct);
|
||||||
|
int _getopt_internal_r_w(int argc, wchar_t *const *argv, const wchar_t *optstring, const struct option_w *longopts, int *longind, int long_only, struct _getopt_data_w *d, int posixly_correct) {
|
||||||
|
int print_errors = d->opterr;
|
||||||
|
if (argc < 1)
|
||||||
|
return -1;
|
||||||
|
d->optarg = NULL;
|
||||||
|
if (d->optind == 0 || !d->__initialized)
|
||||||
|
optstring = _getopt_initialize_w(optstring, d, posixly_correct);
|
||||||
|
else if (optstring[0] == L'-' || optstring[0] == L'+')
|
||||||
|
optstring++;
|
||||||
|
if (optstring[0] == L':')
|
||||||
|
print_errors = 0;
|
||||||
|
#define NONOPTION_P (argv[d->optind][0] != L'-' || argv[d->optind][1] == L'\0')
|
||||||
|
|
||||||
|
if (d->__nextchar == NULL || *d->__nextchar == L'\0') {
|
||||||
|
if (d->__last_nonopt > d->optind)
|
||||||
|
d->__last_nonopt = d->optind;
|
||||||
|
if (d->__first_nonopt > d->optind)
|
||||||
|
d->__first_nonopt = d->optind;
|
||||||
|
if (d->__ordering == PERMUTE) {
|
||||||
|
if (d->__first_nonopt != d->__last_nonopt && d->__last_nonopt != d->optind)
|
||||||
|
exchange_w(const_cast<wchar_t **>(argv), d);
|
||||||
|
else if (d->__last_nonopt != d->optind)
|
||||||
|
d->__first_nonopt = d->optind;
|
||||||
|
while (d->optind < argc && NONOPTION_P)
|
||||||
|
d->optind++;
|
||||||
|
d->__last_nonopt = d->optind;
|
||||||
|
}
|
||||||
|
if (d->optind != argc && !wcscmp(argv[d->optind], L"--")) {
|
||||||
|
d->optind++;
|
||||||
|
if (d->__first_nonopt != d->__last_nonopt && d->__last_nonopt != d->optind)
|
||||||
|
exchange_w(const_cast<wchar_t **>(argv), d);
|
||||||
|
else if (d->__first_nonopt == d->__last_nonopt)
|
||||||
|
d->__first_nonopt = d->optind;
|
||||||
|
d->__last_nonopt = argc;
|
||||||
|
d->optind = argc;
|
||||||
|
}
|
||||||
|
if (d->optind == argc) {
|
||||||
|
if (d->__first_nonopt != d->__last_nonopt)
|
||||||
|
d->optind = d->__first_nonopt;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (NONOPTION_P) {
|
||||||
|
if (d->__ordering == REQUIRE_ORDER)
|
||||||
|
return -1;
|
||||||
|
d->optarg = argv[d->optind++];
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (longopts) {
|
||||||
|
if (argv[d->optind][1] == L'-') {
|
||||||
|
d->__nextchar = argv[d->optind] + 2;
|
||||||
|
return process_long_option_w(argc, const_cast<wchar_t **>(argv), optstring, longopts, longind, long_only, d, print_errors, L"--");
|
||||||
|
}
|
||||||
|
if (long_only && (argv[d->optind][2] || !wcschr(optstring, argv[d->optind][1]))) {
|
||||||
|
int code;
|
||||||
|
d->__nextchar = argv[d->optind] + 1;
|
||||||
|
code = process_long_option_w(argc, const_cast<wchar_t **>(argv), optstring, longopts, longind, long_only, d, print_errors, L"-");
|
||||||
|
if (code != -1)
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d->__nextchar = argv[d->optind] + 1;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
wchar_t c = *d->__nextchar++;
|
||||||
|
const wchar_t *temp = wcschr(optstring, c);
|
||||||
|
if (*d->__nextchar == L'\0')
|
||||||
|
++d->optind;
|
||||||
|
if (temp == NULL || c == L':' || c == L';') {
|
||||||
|
if (print_errors)
|
||||||
|
fwprintf(stderr, L"%s: invalid option -- '%c'\n", argv[0], c);
|
||||||
|
d->optopt = c;
|
||||||
|
return L'?';
|
||||||
|
}
|
||||||
|
if (temp[0] == L'W' && temp[1] == L';' && longopts != NULL) {
|
||||||
|
if (*d->__nextchar != L'\0')
|
||||||
|
d->optarg = d->__nextchar;
|
||||||
|
else if (d->optind == argc) {
|
||||||
|
if (print_errors)
|
||||||
|
fwprintf(stderr, L"%s: option requires an argument -- '%c'\n", argv[0], c);
|
||||||
|
d->optopt = c;
|
||||||
|
if (optstring[0] == L':')
|
||||||
|
c = L':';
|
||||||
|
else
|
||||||
|
c = L'?';
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
d->optarg = argv[d->optind];
|
||||||
|
d->__nextchar = d->optarg;
|
||||||
|
d->optarg = NULL;
|
||||||
|
return process_long_option_w(argc, const_cast<wchar_t **>(argv), optstring, longopts, longind,
|
||||||
|
0, d, print_errors, L"-W ");
|
||||||
|
}
|
||||||
|
if (temp[1] == L':') {
|
||||||
|
if (temp[2] == L':') {
|
||||||
|
if (*d->__nextchar != L'\0') {
|
||||||
|
d->optarg = d->__nextchar;
|
||||||
|
d->optind++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
d->optarg = NULL;
|
||||||
|
d->__nextchar = NULL;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (*d->__nextchar != L'\0') {
|
||||||
|
d->optarg = d->__nextchar;
|
||||||
|
d->optind++;
|
||||||
|
}
|
||||||
|
else if (d->optind == argc) {
|
||||||
|
if (print_errors)
|
||||||
|
fwprintf(stderr, L"%s: option requires an argument -- '%c'\n", argv[0], c);
|
||||||
|
d->optopt = c;
|
||||||
|
if (optstring[0] == L':')
|
||||||
|
c = L':';
|
||||||
|
else
|
||||||
|
c = L'?';
|
||||||
|
}
|
||||||
|
else
|
||||||
|
d->optarg = argv[d->optind++];
|
||||||
|
d->__nextchar = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int _getopt_internal_w(int argc, wchar_t *const *argv, const wchar_t *optstring, const struct option_w *longopts, int *longind, int long_only, int posixly_correct);
|
||||||
|
int _getopt_internal_w(int argc, wchar_t *const *argv, const wchar_t *optstring, const struct option_w *longopts, int *longind, int long_only, int posixly_correct) {
|
||||||
|
int result;
|
||||||
|
getopt_data_w.optind = optind;
|
||||||
|
getopt_data_w.opterr = opterr;
|
||||||
|
result = _getopt_internal_r_w(argc, argv, optstring, longopts, longind, long_only, &getopt_data_w, posixly_correct);
|
||||||
|
optind = getopt_data_w.optind;
|
||||||
|
optarg_w = getopt_data_w.optarg;
|
||||||
|
optopt = getopt_data_w.optopt;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
int getopt_w(int argc, wchar_t *const *argv, const wchar_t *optstring) _GETOPT_THROW {
|
||||||
|
return _getopt_internal_w(argc, argv, optstring, static_cast<const struct option_w *>(0), static_cast<int *>(0), 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int getopt_long_w(int argc, wchar_t *const *argv, const wchar_t *options, const struct option_w *long_options, int *opt_index) _GETOPT_THROW {
|
||||||
|
return _getopt_internal_w(argc, argv, options, long_options, opt_index, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int getopt_long_only_w(int argc, wchar_t *const *argv, const wchar_t *options, const struct option_w *long_options, int *opt_index) _GETOPT_THROW {
|
||||||
|
return _getopt_internal_w(argc, argv, options, long_options, opt_index, 1, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int _getopt_long_r_w(int argc, wchar_t *const *argv, const wchar_t *options, const struct option_w *long_options, int *opt_index, struct _getopt_data_w *d);
|
||||||
|
int _getopt_long_r_w(int argc, wchar_t *const *argv, const wchar_t *options, const struct option_w *long_options, int *opt_index, struct _getopt_data_w *d) {
|
||||||
|
return _getopt_internal_r_w(argc, argv, options, long_options, opt_index, 0, d, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int _getopt_long_only_r_w(int argc, wchar_t *const *argv, const wchar_t *options, const struct option_w *long_options, int *opt_index, struct _getopt_data_w *d);
|
||||||
|
int _getopt_long_only_r_w(int argc, wchar_t *const *argv, const wchar_t *options, const struct option_w *long_options, int *opt_index, struct _getopt_data_w *d) {
|
||||||
|
return _getopt_internal_r_w(argc, argv, options, long_options, opt_index, 1, d, 0);
|
||||||
|
}
|
||||||
135
3rdparty/getopt/getopt.h
vendored
Normal file
135
3rdparty/getopt/getopt.h
vendored
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
/* Getopt for Microsoft C
|
||||||
|
This code is a modification of the Free Software Foundation, Inc.
|
||||||
|
Getopt library for parsing command line argument the purpose was
|
||||||
|
to provide a Microsoft Visual C friendly derivative. This code
|
||||||
|
provides functionality for both Unicode and Multibyte builds.
|
||||||
|
|
||||||
|
Date: 02/03/2011 - Ludvik Jerabek - Initial Release
|
||||||
|
Version: 1.1
|
||||||
|
Comment: Supports getopt, getopt_long, and getopt_long_only
|
||||||
|
and POSIXLY_CORRECT environment flag
|
||||||
|
License: LGPL
|
||||||
|
|
||||||
|
Revisions:
|
||||||
|
|
||||||
|
02/03/2011 - Ludvik Jerabek - Initial Release
|
||||||
|
02/20/2011 - Ludvik Jerabek - Fixed compiler warnings at Level 4
|
||||||
|
07/05/2011 - Ludvik Jerabek - Added no_argument, required_argument, optional_argument defs
|
||||||
|
08/03/2011 - Ludvik Jerabek - Fixed non-argument runtime bug which caused runtime exception
|
||||||
|
08/09/2011 - Ludvik Jerabek - Added code to export functions for DLL and LIB
|
||||||
|
02/15/2012 - Ludvik Jerabek - Fixed _GETOPT_THROW definition missing in implementation file
|
||||||
|
08/01/2012 - Ludvik Jerabek - Created separate functions for char and wchar_t characters so single dll can do both unicode and ansi
|
||||||
|
10/15/2012 - Ludvik Jerabek - Modified to match latest GNU features
|
||||||
|
06/19/2015 - Ludvik Jerabek - Fixed maximum option limitation caused by option_a (255) and option_w (65535) structure val variable
|
||||||
|
09/24/2022 - Ludvik Jerabek - Updated to match most recent getopt release
|
||||||
|
09/25/2022 - Ludvik Jerabek - Fixed memory allocation (malloc call) issue for wchar_t*
|
||||||
|
|
||||||
|
**DISCLAIMER**
|
||||||
|
THIS MATERIAL IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EITHER EXPRESS OR IMPLIED, INCLUDING, BUT Not LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE, OR NON-INFRINGEMENT. SOME JURISDICTIONS DO NOT ALLOW THE
|
||||||
|
EXCLUSION OF IMPLIED WARRANTIES, SO THE ABOVE EXCLUSION MAY NOT
|
||||||
|
APPLY TO YOU. IN NO EVENT WILL I BE LIABLE TO ANY PARTY FOR ANY
|
||||||
|
DIRECT, INDIRECT, SPECIAL OR OTHER CONSEQUENTIAL DAMAGES FOR ANY
|
||||||
|
USE OF THIS MATERIAL INCLUDING, WITHOUT LIMITATION, ANY LOST
|
||||||
|
PROFITS, BUSINESS INTERRUPTION, LOSS OF PROGRAMS OR OTHER DATA ON
|
||||||
|
YOUR INFORMATION HANDLING SYSTEM OR OTHERWISE, EVEN If WE ARE
|
||||||
|
EXPRESSLY ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
||||||
|
*/
|
||||||
|
#ifndef __GETOPT_H_
|
||||||
|
#define __GETOPT_H_
|
||||||
|
|
||||||
|
#ifdef _GETOPT_API
|
||||||
|
# undef _GETOPT_API
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(EXPORTS_GETOPT) && defined(STATIC_GETOPT)
|
||||||
|
# error "The preprocessor definitions of EXPORTS_GETOPT and STATIC_GETOPT can only be used individually"
|
||||||
|
#elif defined(STATIC_GETOPT)
|
||||||
|
# define _GETOPT_API
|
||||||
|
#elif defined(EXPORTS_GETOPT)
|
||||||
|
# pragma message("Exporting getopt library")
|
||||||
|
# define _GETOPT_API __declspec(dllexport)
|
||||||
|
#else
|
||||||
|
# pragma message("Importing getopt library")
|
||||||
|
# define _GETOPT_API __declspec(dllimport)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Change behavior for C\C++
|
||||||
|
#ifdef __cplusplus
|
||||||
|
# define _BEGIN_EXTERN_C extern "C" {
|
||||||
|
# define _END_EXTERN_C }
|
||||||
|
# define _GETOPT_THROW throw()
|
||||||
|
#else
|
||||||
|
# define _BEGIN_EXTERN_C
|
||||||
|
# define _END_EXTERN_C
|
||||||
|
# define _GETOPT_THROW
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Standard GNU options
|
||||||
|
#define null_argument 0 /*Argument Null*/
|
||||||
|
#define no_argument 0 /*Argument Switch Only*/
|
||||||
|
#define required_argument 1 /*Argument Required*/
|
||||||
|
#define optional_argument 2 /*Argument Optional*/
|
||||||
|
|
||||||
|
// Shorter Options
|
||||||
|
#define ARG_NULL 0 /*Argument Null*/
|
||||||
|
#define ARG_NONE 0 /*Argument Switch Only*/
|
||||||
|
#define ARG_REQ 1 /*Argument Required*/
|
||||||
|
#define ARG_OPT 2 /*Argument Optional*/
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
#include <wchar.h>
|
||||||
|
|
||||||
|
_BEGIN_EXTERN_C
|
||||||
|
|
||||||
|
extern _GETOPT_API int optind;
|
||||||
|
extern _GETOPT_API int opterr;
|
||||||
|
extern _GETOPT_API int optopt;
|
||||||
|
|
||||||
|
// Ansi
|
||||||
|
struct option_a {
|
||||||
|
const char *name;
|
||||||
|
int has_arg;
|
||||||
|
int *flag;
|
||||||
|
int val;
|
||||||
|
};
|
||||||
|
extern _GETOPT_API char *optarg_a;
|
||||||
|
extern _GETOPT_API int getopt_a(int argc, char *const *argv, const char *optstring) _GETOPT_THROW;
|
||||||
|
extern _GETOPT_API int getopt_long_a(int argc, char *const *argv, const char *options, const struct option_a *long_options, int *opt_index) _GETOPT_THROW;
|
||||||
|
extern _GETOPT_API int getopt_long_only_a(int argc, char *const *argv, const char *options, const struct option_a *long_options, int *opt_index) _GETOPT_THROW;
|
||||||
|
|
||||||
|
// Unicode
|
||||||
|
struct option_w {
|
||||||
|
const wchar_t *name;
|
||||||
|
int has_arg;
|
||||||
|
int *flag;
|
||||||
|
int val;
|
||||||
|
};
|
||||||
|
extern _GETOPT_API wchar_t *optarg_w;
|
||||||
|
extern _GETOPT_API int getopt_w(int argc, wchar_t *const *argv, const wchar_t *optstring) _GETOPT_THROW;
|
||||||
|
extern _GETOPT_API int getopt_long_w(int argc, wchar_t *const *argv, const wchar_t *options, const struct option_w *long_options, int *opt_index) _GETOPT_THROW;
|
||||||
|
extern _GETOPT_API int getopt_long_only_w(int argc, wchar_t *const *argv, const wchar_t *options, const struct option_w *long_options, int *opt_index) _GETOPT_THROW;
|
||||||
|
|
||||||
|
_END_EXTERN_C
|
||||||
|
|
||||||
|
#undef _BEGIN_EXTERN_C
|
||||||
|
#undef _END_EXTERN_C
|
||||||
|
#undef _GETOPT_THROW
|
||||||
|
#undef _GETOPT_API
|
||||||
|
|
||||||
|
#ifdef _UNICODE
|
||||||
|
# define getopt getopt_w
|
||||||
|
# define getopt_long getopt_long_w
|
||||||
|
# define getopt_long_only getopt_long_only_w
|
||||||
|
# define option option_w
|
||||||
|
# define optarg optarg_w
|
||||||
|
#else
|
||||||
|
# define getopt getopt_a
|
||||||
|
# define getopt_long getopt_long_a
|
||||||
|
# define getopt_long_only getopt_long_only_a
|
||||||
|
# define option option_a
|
||||||
|
# define optarg optarg_a
|
||||||
|
#endif
|
||||||
|
#endif // __GETOPT_H_
|
||||||
8
3rdparty/kdsingleapplication/CMakeLists.txt
vendored
Normal file
8
3rdparty/kdsingleapplication/CMakeLists.txt
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.13)
|
||||||
|
set(SOURCES KDSingleApplication/src/kdsingleapplication.cpp KDSingleApplication/src/kdsingleapplication_localsocket.cpp)
|
||||||
|
set(HEADERS KDSingleApplication/src/kdsingleapplication.h KDSingleApplication/src/kdsingleapplication_localsocket_p.h)
|
||||||
|
qt_wrap_cpp(MOC ${HEADERS})
|
||||||
|
add_library(kdsingleapplication STATIC ${SOURCES} ${MOC})
|
||||||
|
target_compile_definitions(kdsingleapplication PRIVATE -DKDSINGLEAPPLICATION_STATIC_BUILD)
|
||||||
|
target_include_directories(kdsingleapplication PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
|
target_link_libraries(kdsingleapplication PUBLIC Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Network)
|
||||||
1
3rdparty/kdsingleapplication/KDSingleApplication
vendored
Submodule
1
3rdparty/kdsingleapplication/KDSingleApplication
vendored
Submodule
Submodule 3rdparty/kdsingleapplication/KDSingleApplication added at cb0c664b40
62
Brewfile
62
Brewfile
@@ -1,62 +0,0 @@
|
|||||||
# Strawberry Music Player (macOS) - Homebrew Bundle
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# brew bundle --file Brewfile
|
|
||||||
#
|
|
||||||
# Notes:
|
|
||||||
# - This is intended for macOS (Apple Silicon or Intel).
|
|
||||||
# - Some Strawberry features are optional and will auto-disable if deps are missing.
|
|
||||||
|
|
||||||
# Build tooling
|
|
||||||
brew "cmake"
|
|
||||||
brew "pkg-config"
|
|
||||||
brew "ninja"
|
|
||||||
|
|
||||||
# Optional (developer): unit tests
|
|
||||||
brew "googletest"
|
|
||||||
|
|
||||||
# Core runtime/build dependencies (required by CMakeLists.txt)
|
|
||||||
brew "qt" # Qt 6 (Core/Gui/Widgets/Network/Sql/Concurrent)
|
|
||||||
brew "vulkan-headers" # helps Qt6Gui's WrapVulkanHeaders dependency on some setups
|
|
||||||
brew "boost"
|
|
||||||
brew "icu4c"
|
|
||||||
brew "glib" # provides glib-2.0 + gobject-2.0 (via pkg-config)
|
|
||||||
brew "glib-networking" # TLS + GIO modules (helps macOS bundling via dist/macos/macgstcopy.sh)
|
|
||||||
brew "sqlite"
|
|
||||||
brew "taglib"
|
|
||||||
brew "gstreamer"
|
|
||||||
|
|
||||||
# Strawberry requires KDAB's KDSingleApplication (CMake package name: KDSingleApplication-qt6).
|
|
||||||
# Homebrew core doesn't consistently provide it, so this repo includes a local formula.
|
|
||||||
# Homebrew requires formulae to be installed from a tap; we tap *this repo* via file://.
|
|
||||||
# Use the Brewfile's directory (repo root) rather than the current working directory,
|
|
||||||
# so `brew bundle --file /path/to/Brewfile` works no matter where you run it from.
|
|
||||||
tap "strawberry/local", "file://#{File.expand_path(__dir__)}"
|
|
||||||
brew "strawberry/local/kdsingleapplication-qt6"
|
|
||||||
brew "strawberry/local/qtsparkle-qt6" # optional: QtSparkle integration
|
|
||||||
brew "strawberry/local/sparkle-framework" # optional: Sparkle integration (framework)
|
|
||||||
brew "strawberry/local/macdeploycheck" # optional: enables CMake target 'deploycheck' (sanity checks deployed .app)
|
|
||||||
|
|
||||||
# Recommended GStreamer plugin sets for broad codec support (matches README guidance)
|
|
||||||
brew "gst-plugins-base"
|
|
||||||
brew "gst-plugins-good"
|
|
||||||
brew "gst-plugins-bad"
|
|
||||||
brew "gst-plugins-ugly"
|
|
||||||
brew "gst-libav"
|
|
||||||
|
|
||||||
# Optional features (silences CMake warnings / enables extra functionality)
|
|
||||||
brew "rapidjson" # enables Discord Rich Presence (DISCORD_RPC)
|
|
||||||
brew "google-sparsehash" # enables stream tagreader (STREAMTAGREADER / libsparsehash)
|
|
||||||
brew "chromaprint" # enables MusicBrainz + song fingerprinting
|
|
||||||
brew "fftw" # enables Moodbar (fftw3)
|
|
||||||
brew "libebur128" # enables EBU R 128 loudness normalization
|
|
||||||
brew "libcdio" # enables Audio CD support
|
|
||||||
brew "libmtp" # enables MTP device support
|
|
||||||
brew "strawberry/local/libgpod" # enables iPod classic support (Homebrew core doesn't provide libgpod)
|
|
||||||
|
|
||||||
# Helpful for Strawberry's macOS "deploy" target (GStreamer dynamically loads libsoup)
|
|
||||||
brew "libsoup"
|
|
||||||
|
|
||||||
# Optional: enable building the CMake "dmg" target (cmake/Dmg.cmake)
|
|
||||||
brew "create-dmg"
|
|
||||||
|
|
||||||
1785
CMakeLists.txt
1785
CMakeLists.txt
File diff suppressed because it is too large
Load Diff
268
Changelog
268
Changelog
@@ -2,273 +2,9 @@ Strawberry Music Player
|
|||||||
=======================
|
=======================
|
||||||
ChangeLog
|
ChangeLog
|
||||||
|
|
||||||
Version 1.2.17 (2026.01.18):
|
|
||||||
|
|
||||||
* Avoid re-scan of restored songs unless mtime is changed (#1819)
|
|
||||||
* Skip existing files when organizing if not overwriting (#1484)
|
|
||||||
* Fixed cursor highlight disappearing off-screen when using down cursor (#1489)
|
|
||||||
* Fixed CD playback only working for the first optical drive (#1852)
|
|
||||||
* Fixed possible race-condition when switching tracks (#1863)
|
|
||||||
* Fixed possible file descriptor exhaustion by using shared thread pool (#1687)
|
|
||||||
* Don't automatically sort playlist with the auto sort option before it's fully loaded (#1690)
|
|
||||||
* Fixed network features stop working after computer suspends and resumes (#1521)
|
|
||||||
* Fixed crash on exit after Qobuz login
|
|
||||||
* Added tag editor option to select ID3v2 version (#1861)
|
|
||||||
* Fixed Qobuz authentication and added automatic credential fetching (#1898)
|
|
||||||
* Fixed playback stopping after deleting a song from disk via context menu (#1783)
|
|
||||||
* Added option to restore smart playlists to the defaults (#1848)
|
|
||||||
* Fixed possible race condition in pipeline destructor (#1875)
|
|
||||||
* Fixed buffering issue near track end during gapless playback (#1725)
|
|
||||||
* Fixed duplicate collection entries for the same artist if they have different sort tags (#1899)
|
|
||||||
* Defer playcount and rating tag writes for currently playing Ogg songs to prevent playback shutter (#1816)
|
|
||||||
* Fixed tag editing not working for Opus sort tags (#1929)
|
|
||||||
* Show playlist load errors (#1470)
|
|
||||||
* Fallback to delete if moving to trash fails (#1679)
|
|
||||||
* Prefer filenames with "front" or "cover" in the filename for album cover art for songs outside of the collection (#1745)
|
|
||||||
* Fixed collection enter/return behavior to respect double-click settings (#1691)
|
|
||||||
* Added tree view mode to files tab (#1922)
|
|
||||||
* Include .webp in allowed extensions for album covers (#1941)
|
|
||||||
* Exit gracefully on SIGTERM signal for Unix systems (#1942)
|
|
||||||
* Optimize the collection scanning process by deferring media file validation from the initial directory scan (#1954)
|
|
||||||
* Fixed collection scan not finding new directories in the top level collection directory when the mountpoint is restored (#1914)
|
|
||||||
* Added genre metadata parsing for Tidal, Qobuz and Spotify (#1913)
|
|
||||||
* Allow editing metadata for stream songs (#1913)
|
|
||||||
* Optimized collection/playlist filtering
|
|
||||||
* Added sort tags to collection/playlist filtering (#1966)
|
|
||||||
|
|
||||||
Version 1.2.16 (2025.12.16):
|
|
||||||
|
|
||||||
* Make Discord Rich presence use filename if song title is missing
|
|
||||||
* Added better error message when a GStreamer plugin is missing
|
|
||||||
* Preserve track order in album shuffle mode when restarting playback (#1623)
|
|
||||||
* Possible fixes for context word wrap
|
|
||||||
* Added lyrics from lrclib.net
|
|
||||||
* Added option to turn off the use of sort tags for the collection
|
|
||||||
* Fixed Spotify login
|
|
||||||
* Fixed error dialog shown minimized if another Strawberry window than the mainwindow was active
|
|
||||||
* Fixed seeking to the end of the track and back causing seeking to stop working (#1675)
|
|
||||||
* Set current index when automatically selecting track (#1825)
|
|
||||||
* Make icon size for shuffle and repeat buttons adjust to screen resolution (#1838)
|
|
||||||
* Fixed song being removed from playlist when dragging to another application (#1815)
|
|
||||||
* Don't automatically scroll on dynamic playlists (#1427)
|
|
||||||
|
|
||||||
Version 1.2.15 (2025.11.25):
|
|
||||||
|
|
||||||
* Fixed system default language not respected
|
|
||||||
* Fixed length filter search
|
|
||||||
* Fixed playlist parser converting Spotify URL's
|
|
||||||
* Removed use of deprecated QStyle::State_Editing
|
|
||||||
* Ignore connection closed errors for ListenBrainz
|
|
||||||
* (Windows) Support building with vcpkg unofficial::getopt-win32::getopt
|
|
||||||
|
|
||||||
Version 1.2.14 (2025.10.25):
|
|
||||||
|
|
||||||
Bugfixes:
|
|
||||||
* Fixed showing error dialog minimized when main window is not current active window (#1739)
|
|
||||||
* Fixed Discord timestamp update when seeking (#1813)
|
|
||||||
* Fixed CD metadata lookup to respect MusicBrainz rate limiting
|
|
||||||
* Fixed Tidal Open API cover provider
|
|
||||||
* (Windows) Fixed device selection with WASAPI2
|
|
||||||
|
|
||||||
Enhancements/Other:
|
|
||||||
* Removed libre.fm support
|
|
||||||
* Rewrote MusicBrainzClient to use Json instead of XML
|
|
||||||
* Subsonic will now use cover art from album when available
|
|
||||||
* Added option to remove "Remastered", etc from song titles for Tidal, Qobuz and Spotify
|
|
||||||
* Added webm to supported file extensions
|
|
||||||
* (Windows|MinGW) Added WASAPI2 support
|
|
||||||
* (Windows) Added experimental exclusive mode for WASAPI2
|
|
||||||
|
|
||||||
Version 1.2.13 (2025.08.31):
|
|
||||||
|
|
||||||
Bugfixes:
|
|
||||||
* Fixed playlist alternating row colors no longer working with some styles (#1806)
|
|
||||||
* Fixed "Open Audio CD" no longer working (#1803)
|
|
||||||
* Fixed systemtray icon playback status not working with scaling (#1782)
|
|
||||||
* Fixed build without MusicBrainz (#1799)
|
|
||||||
* Fixed build without MTP (#1804)
|
|
||||||
|
|
||||||
Enhancements:
|
|
||||||
* Added Discord status text option (#1796)
|
|
||||||
* Read Vorbis/FLAC "Other" embedded covers if front cover is not available (#1793)
|
|
||||||
|
|
||||||
Version 1.2.12 (2025.08.12):
|
|
||||||
|
|
||||||
Bugfixes:
|
|
||||||
* Fixed scrobbling for radio streams.
|
|
||||||
* Fixed CDDA memory leaks.
|
|
||||||
* Fixed device view CDDA loading (#1676).
|
|
||||||
* Fixed collection directory editing (#1767).
|
|
||||||
* Fixed devices sometimes being duplicated in the database.
|
|
||||||
* Fixed alternating playlist row colors with Windows 11 style.
|
|
||||||
* Fixed broken file filter for GME formats.
|
|
||||||
* Fixed collection scanning for GME formats.
|
|
||||||
* Fixed Chartlyrics.
|
|
||||||
* Fixed network cache file descriptor leak on lyrics search with workaround for QTBUG-135641.
|
|
||||||
* Fixed parsing Tidal urls with certain stream URL replies.
|
|
||||||
* Fixed pixelated window icon on Wayland (#1753).
|
|
||||||
* Fixed saving collection grouping with special characters in the name (#1758).
|
|
||||||
* Fixed Spotify token not automatically updated on renewal when playing (#1769).
|
|
||||||
* (macOS/Windows) Fixed network cache file descriptor leak with patch for QTBUG-135641.
|
|
||||||
* (Windows|MSVC) Fixed installer to not restart the computer after installing Visual C++ Redistributable.
|
|
||||||
|
|
||||||
Enhancements:
|
|
||||||
* Implemented edit tag dialog reset for year, track, disc and rating.
|
|
||||||
* Added ALAC to supported filetypes for iPods.
|
|
||||||
* Added CD-TEXT support.
|
|
||||||
* Added back Genius lyrics.
|
|
||||||
* Added support for reporting more info to ListenBrainz.
|
|
||||||
* Added support for BPM, mood and initial key tags.
|
|
||||||
* Added support for sort tags to collection, playlists and smart playlists.
|
|
||||||
|
|
||||||
Version 1.2.11 (2025.05.15):
|
|
||||||
|
|
||||||
* Fixed playlist songs sometimes not updated with new cover.
|
|
||||||
* Fixed context album cover showing even when it's disabled in the setting (#1744).
|
|
||||||
* Fixed crash when dragging songs to a closed playlist (#1741).
|
|
||||||
* Enable startup notify in desktop file.
|
|
||||||
* (Windows|MSVC) Add experimental support for native ARM64 builds.
|
|
||||||
* (Windows|MinGW) Fixed crash on exit.
|
|
||||||
|
|
||||||
Version 1.2.10 (2025.04.18):
|
|
||||||
|
|
||||||
Bugfixes:
|
|
||||||
* Fixed Discord rich presence showing bogus artist and album.
|
|
||||||
* Fixed incorrect ID3v2 comment tag.
|
|
||||||
* (macOS|Windows MSVC) Fixed stuck playback of some streams.
|
|
||||||
|
|
||||||
Enhancements:
|
|
||||||
* Removed Genius lyrics (longer working properly because of website changes).
|
|
||||||
* (macOS|Windows MSVC) Added back Spotify
|
|
||||||
|
|
||||||
Version 1.2.9 (2025.04.08):
|
|
||||||
|
|
||||||
Bugfixes:
|
|
||||||
* Fixed subsonic parse error (#1719).
|
|
||||||
* Fixed Deezer cover provider parse error (#1716).
|
|
||||||
* Fixed last.fm import progress.
|
|
||||||
* (Windows|MinGW) Switched from winpthreads to win32 threads, winpthreads are no longer working with Qt as of version 6.9 (QTBUG-131892).
|
|
||||||
|
|
||||||
Enhancements:
|
|
||||||
* Added option to disable playbin3.
|
|
||||||
|
|
||||||
Version 1.2.8 (2025.04.05):
|
|
||||||
|
|
||||||
Bugfixes:
|
|
||||||
* Added "HI_RES_LOSSLESS" for Tidal quality setting.
|
|
||||||
* Increased backend settings device lineedit height.
|
|
||||||
* Possible fix for KGlobalAccel shortcuts sometimes not working.
|
|
||||||
|
|
||||||
Enhancements:
|
|
||||||
* Removed deprecated Tidal username/password login.
|
|
||||||
* Turned off "Grey out unavailable songs in playlists on startup" by default.
|
|
||||||
* Added support for reading tags from streams.
|
|
||||||
* Added tooltips in equalizer, backend and appearance settings.
|
|
||||||
* Added full tag support for AIFF including embedded covers.
|
|
||||||
* Use card ID instead of index for ALSA devices.
|
|
||||||
* Removed KDSingleApplication from 3rdparty.
|
|
||||||
* Support arbitrarily large EBU R 128 loudness normalization.
|
|
||||||
|
|
||||||
New features:
|
|
||||||
* Added Discord rich presence support.
|
|
||||||
|
|
||||||
Version 1.2.7 (2025.01.31):
|
|
||||||
|
|
||||||
Bugfixes:
|
|
||||||
* Fixed strawberry exiting when clicking tray icon.
|
|
||||||
* Fixed Clementine import script errors.
|
|
||||||
* Disabled OSD Pretty on Wayland since it's not working properly.
|
|
||||||
|
|
||||||
Enhancements:
|
|
||||||
* Only maximize error dialog if Strawberry is the active window (#1627).
|
|
||||||
* Added QPA Platform Native Interface as optional component.
|
|
||||||
|
|
||||||
Version 1.2.6 (2025.01.17):
|
|
||||||
|
|
||||||
Bugfixes:
|
|
||||||
* Fixed dragging songs from playlist to queue.
|
|
||||||
|
|
||||||
Version 1.2.5 (2025.01.17):
|
|
||||||
|
|
||||||
Bugfixes:
|
|
||||||
* Fixed crash when saving playcount or rating to file (#1633).
|
|
||||||
* Fixed QFile::open failing in unit tests.
|
|
||||||
* Fixed playlist sequence settings saved to wrong configuration file (#1649).
|
|
||||||
|
|
||||||
Enhancements:
|
|
||||||
* Fixed use of deprecated GIO functions with GLib 2.84 and newer.
|
|
||||||
* (macOS) Added back Sparkle updater to check for new releases.
|
|
||||||
|
|
||||||
Version 1.2.4 (2025.01.10):
|
|
||||||
|
|
||||||
Bugfixes:
|
|
||||||
* Fixed Spotify songs not being available for scrobbling.
|
|
||||||
* Fixed leading "A" and "The" articles being skipped for album sort text.
|
|
||||||
* Fixed thread safety issue when validating playlist songs on startup.
|
|
||||||
* Fixed filter search not ignoring space after colon when using column based search.
|
|
||||||
* Fixed KGlobalAccel to use capitalized application name.
|
|
||||||
* Fixed slash not properly handled when saving a playlist (#1624).
|
|
||||||
* (Unix) Fixed collection scanner so it ignores special filesystem paths (/sys, /proc, /run, etc) (#1615).
|
|
||||||
* (Windows) Fixed smart playlist wizard not respecting dark mode with Windows 11 style (#1639).
|
|
||||||
|
|
||||||
Enhancements:
|
|
||||||
* Use XSPF "title" as playlist name when loading and saving playlists (#1624).
|
|
||||||
* Added support for using album ID when receving album covers for Subsonic songs (#1636).
|
|
||||||
* Added option for preserving directory structure when trascoding songs (#1637).
|
|
||||||
* (Windows) Always run MSVC runtime installer to possible fix issues when there is an older runtime installed.
|
|
||||||
|
|
||||||
Version 1.2.3 (2024.12.08):
|
|
||||||
|
|
||||||
Bugfixes:
|
|
||||||
* Fixed libcdio NULL related compilation error on FreeBSD (#1610).
|
|
||||||
* Fixed missing seek when starting playback of a CUE song (#1568).
|
|
||||||
* Fixed "QDBusObjectPath: invalid path" error.
|
|
||||||
|
|
||||||
Version 1.2.2 (2024.11.23):
|
|
||||||
|
|
||||||
Bugfixes:
|
|
||||||
|
|
||||||
* Fixed crash when creating a new smart playlist (#1609).
|
|
||||||
* Fixed last playlist column being added when dragging a song and switching playlists.
|
|
||||||
|
|
||||||
Version 1.2.1 (2024.11.21):
|
|
||||||
|
|
||||||
This release features major restructuring of the codebase, moving source files,
|
|
||||||
rewriting CMake build files, dropping Qt 5 support, external tagreader,
|
|
||||||
and dropping some unmaintained parts such as VLC.
|
|
||||||
|
|
||||||
Bugfixes:
|
|
||||||
|
|
||||||
* Fixed playback of CUE continuing to play from the same file after the song has finished playing (#1568).
|
|
||||||
* Fixed updating collection song sort text when disc is changed.
|
|
||||||
* Fixed current playing file left open when the next track errored (#1582).
|
|
||||||
* Fixed filter search not finding song containing uppercase "A" (#1599).
|
|
||||||
* Fixed crash when removing album from playlist when using shuffle albums (#1588).
|
|
||||||
* Fixed IDv3 MBID's tags with multiple entries being ignored.
|
|
||||||
* Fixed crash when enabling Tidal, Spotify, Qobuz or Subsonic services.
|
|
||||||
* Fixed passing filenames to strawberry on command line not resolving to absolute paths.
|
|
||||||
* (macOS) Fixed program not starting for users with long usernames.
|
|
||||||
* (macoS) Fixed crash when pressing caps lock (#1606).
|
|
||||||
* (macOS) Remove "song progress on taskbar" option in behaviour settings.
|
|
||||||
|
|
||||||
Enhancements:
|
|
||||||
|
|
||||||
* Resolve symbolic links when dragging files to the playlist to match collection song.
|
|
||||||
* Replaced Spotify username/password with access token.
|
|
||||||
* Require Qt 6.4 or higher and drop support for Qt 5.
|
|
||||||
* Require TagLib 1.12 or higher.
|
|
||||||
* Use Qt stringliterals.
|
|
||||||
* Move gstfastspectrum to src.
|
|
||||||
* Use standard user temp location for current album cover.
|
|
||||||
* Removed old MacFSListener.
|
|
||||||
* Removed external tagreader and protobuf dependency.
|
|
||||||
* Removed VLC support.
|
|
||||||
* Ported to Qt translation (.ts) files and removed gettext dependency.
|
|
||||||
* Removed deprecated Gnome/Mate SettingsDaemon global shortcuts.
|
|
||||||
|
|
||||||
Version 1.1.3 (2024.09.21):
|
Version 1.1.3 (2024.09.21):
|
||||||
|
|
||||||
Bugfixes:
|
Bugfixes:
|
||||||
* Fixed gstreamer registry lookup leak in Spotify settings.
|
* Fixed gstreamer registry lookup leak in Spotify settings.
|
||||||
* Fixed all songs in a CUE sheet starting playback at the zero position (#1549).
|
* Fixed all songs in a CUE sheet starting playback at the zero position (#1549).
|
||||||
* Fixed playback going to pause and back to play on song change.
|
* Fixed playback going to pause and back to play on song change.
|
||||||
@@ -343,7 +79,7 @@ Version 1.1.0 (2024.07.14):
|
|||||||
* Only use playbin3 with GStreamer 1.24 and higher, not with GStreamer 1.22 or lower.
|
* Only use playbin3 with GStreamer 1.24 and higher, not with GStreamer 1.22 or lower.
|
||||||
* (macOS/Windows) Fixed dash and hls streaming, plugins were missing.
|
* (macOS/Windows) Fixed dash and hls streaming, plugins were missing.
|
||||||
* (Windows) Fixed incorrect colors in smart playlist wizard with Fusion in dark mode (#1399).
|
* (Windows) Fixed incorrect colors in smart playlist wizard with Fusion in dark mode (#1399).
|
||||||
* (Windows) Fixed update window blocking startup window on launch.
|
* (Windows) Fixed update window blocking sponsor window on startup.
|
||||||
|
|
||||||
Enhancements:
|
Enhancements:
|
||||||
* Improve error messages when connecting and copying to devices.
|
* Improve error messages when connecting and copying to devices.
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
class KdsingleapplicationQt6 < Formula
|
|
||||||
desc "Helper class for single-instance Qt applications (Qt 6 build)"
|
|
||||||
homepage "https://github.com/KDAB/KDSingleApplication"
|
|
||||||
url "https://github.com/KDAB/KDSingleApplication/archive/refs/tags/v1.1.0.tar.gz"
|
|
||||||
sha256 "1f19124c0aa5c6fffee3da174f7d2e091fab6dca1e123da70bb0fe615bfbe3e8"
|
|
||||||
license "MIT"
|
|
||||||
|
|
||||||
depends_on "cmake" => :build
|
|
||||||
depends_on "ninja" => :build
|
|
||||||
depends_on "qt"
|
|
||||||
|
|
||||||
def install
|
|
||||||
args = std_cmake_args + %W[
|
|
||||||
-GNinja
|
|
||||||
-DKDSingleApplication_QT6=ON
|
|
||||||
-DKDSingleApplication_TESTS=OFF
|
|
||||||
-DKDSingleApplication_EXAMPLES=OFF
|
|
||||||
-DKDSingleApplication_DOCS=OFF
|
|
||||||
-DKDSingleApplication_DEVELOPER_MODE=OFF
|
|
||||||
-DKDSingleApplication_STATIC=OFF
|
|
||||||
]
|
|
||||||
|
|
||||||
system "cmake", "-S", ".", "-B", "build", *args
|
|
||||||
system "cmake", "--build", "build"
|
|
||||||
system "cmake", "--install", "build"
|
|
||||||
end
|
|
||||||
|
|
||||||
test do
|
|
||||||
# Verify CMake package is usable via find_package(KDSingleApplication-qt6 CONFIG REQUIRED)
|
|
||||||
(testpath/"CMakeLists.txt").write <<~CMAKE
|
|
||||||
cmake_minimum_required(VERSION 3.16)
|
|
||||||
project(kdsa_test LANGUAGES CXX)
|
|
||||||
find_package(KDSingleApplication-qt6 CONFIG REQUIRED)
|
|
||||||
add_executable(test_kdsa main.cpp)
|
|
||||||
target_link_libraries(test_kdsa PRIVATE KDAB::kdsingleapplication)
|
|
||||||
CMAKE
|
|
||||||
|
|
||||||
(testpath/"main.cpp").write <<~CPP
|
|
||||||
#include <QCoreApplication>
|
|
||||||
int main(int argc, char** argv) {
|
|
||||||
QCoreApplication app(argc, argv);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
CPP
|
|
||||||
|
|
||||||
system "cmake", "-S", ".", "-B", "build",
|
|
||||||
"-DCMAKE_PREFIX_PATH=#{opt_prefix}"
|
|
||||||
system "cmake", "--build", "build"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# kdsingleapplication-qt6 (local Homebrew formula)
|
|
||||||
|
|
||||||
This directory exists to keep any supporting files for the local Homebrew formula
|
|
||||||
next to it (e.g. patches or notes).
|
|
||||||
|
|
||||||
## Install (from this Strawberry repo)
|
|
||||||
|
|
||||||
From the repo root:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
brew tap strawberry/local "file://$PWD"
|
|
||||||
brew install strawberry/local/kdsingleapplication-qt6
|
|
||||||
```
|
|
||||||
|
|
||||||
## Why it exists
|
|
||||||
|
|
||||||
Strawberry’s build requires the CMake package `KDSingleApplication-qt6`, but it is
|
|
||||||
not consistently available via Homebrew core. Shipping a local formula makes the
|
|
||||||
dependency easy to install for anyone building this repo on macOS.
|
|
||||||
|
|
||||||
## Note for local development
|
|
||||||
|
|
||||||
Homebrew taps are Git clones, so the formula must be committed (or pushed to a remote)
|
|
||||||
to be visible to `brew tap`.
|
|
||||||
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
class Libgpod < Formula
|
|
||||||
desc "Library to access the contents of classic iPods"
|
|
||||||
homepage "https://gtkpod.org/libgpod/"
|
|
||||||
url "https://github.com/neuschaefer/libgpod/archive/0dda196286f5e42be89f0b870abd9278213989a5.tar.gz"
|
|
||||||
sha256 "a9809f85b2b763196ac7c94903211a927efd37a24ef39c355c21b4a1bed28e52"
|
|
||||||
license "LGPL-2.0-only"
|
|
||||||
|
|
||||||
depends_on "autoconf" => :build
|
|
||||||
depends_on "automake" => :build
|
|
||||||
depends_on "libtool" => :build
|
|
||||||
depends_on "pkg-config" => :build
|
|
||||||
depends_on "gtk-doc" => :build
|
|
||||||
depends_on "intltool" => :build
|
|
||||||
|
|
||||||
depends_on "glib"
|
|
||||||
depends_on "gdk-pixbuf"
|
|
||||||
depends_on "libplist"
|
|
||||||
depends_on "libxml2"
|
|
||||||
depends_on "sqlite"
|
|
||||||
|
|
||||||
def install
|
|
||||||
# libgpod's configure.ac checks for pkg-config module name "libplist".
|
|
||||||
# Homebrew provides "libplist-2.0", so we provide a tiny shim .pc file to
|
|
||||||
# satisfy the expected name.
|
|
||||||
(buildpath/"brew-pkgconfig").mkpath
|
|
||||||
(buildpath/"brew-pkgconfig/libplist.pc").write <<~EOS
|
|
||||||
prefix=#{Formula["libplist"].opt_prefix}
|
|
||||||
exec_prefix=${prefix}
|
|
||||||
libdir=#{Formula["libplist"].opt_lib}
|
|
||||||
includedir=#{Formula["libplist"].opt_include}
|
|
||||||
|
|
||||||
Name: libplist
|
|
||||||
Description: Apple property list library (Homebrew shim for libgpod)
|
|
||||||
Version: #{Formula["libplist"].version}
|
|
||||||
Libs: -L${libdir} -lplist-2.0
|
|
||||||
Cflags: -I${includedir}
|
|
||||||
EOS
|
|
||||||
|
|
||||||
ENV.prepend_path "PKG_CONFIG_PATH", buildpath/"brew-pkgconfig"
|
|
||||||
|
|
||||||
# Ensure pkg-config can find Homebrew keg .pc files during configure.
|
|
||||||
ENV.prepend_path "PKG_CONFIG_PATH", Formula["libplist"].opt_lib/"pkgconfig"
|
|
||||||
ENV.prepend_path "PKG_CONFIG_PATH", Formula["sqlite"].opt_lib/"pkgconfig"
|
|
||||||
ENV.prepend_path "PKG_CONFIG_PATH", Formula["glib"].opt_lib/"pkgconfig"
|
|
||||||
ENV.prepend_path "PKG_CONFIG_PATH", Formula["glib"].opt_share/"pkgconfig"
|
|
||||||
ENV.prepend_path "PKG_CONFIG_PATH", Formula["gdk-pixbuf"].opt_lib/"pkgconfig"
|
|
||||||
|
|
||||||
# Upstream's autogen.sh is very old and may hardcode ancient automake checks
|
|
||||||
# (e.g. looking for automake-1.7). Using autoreconf is the standard Homebrew
|
|
||||||
# way and works with modern autotools.
|
|
||||||
#
|
|
||||||
# libgpod's build system expects gtk-doc's makefile snippet to exist (gtk-doc.make),
|
|
||||||
# which is normally provided by running gtkdocize.
|
|
||||||
system "gtkdocize", "--copy"
|
|
||||||
|
|
||||||
# libgpod also uses intltool's Autoconf macros (IT_PROG_INTLTOOL). If intltoolize
|
|
||||||
# is not run, the generated ./configure may contain unexpanded macros and fail.
|
|
||||||
system "intltoolize", "--force", "--copy", "--automake"
|
|
||||||
system "autoreconf", "-fiv"
|
|
||||||
|
|
||||||
system "./configure", *std_configure_args,
|
|
||||||
"--disable-dependency-tracking",
|
|
||||||
"--with-hal=no",
|
|
||||||
"--disable-udev",
|
|
||||||
"--without-libimobiledevice",
|
|
||||||
"--with-python=no",
|
|
||||||
"--with-mono=no",
|
|
||||||
"--disable-gtk-doc",
|
|
||||||
"--disable-gtk-doc-html",
|
|
||||||
"--disable-gtk-doc-pdf",
|
|
||||||
"--enable-more-warnings=no"
|
|
||||||
|
|
||||||
system "make", "install"
|
|
||||||
end
|
|
||||||
|
|
||||||
test do
|
|
||||||
# Ensure pkg-config can find the expected module name used by Strawberry.
|
|
||||||
assert_match "libgpod", shell_output("pkg-config --libs libgpod-1.0")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# libgpod (local Homebrew formula)
|
|
||||||
|
|
||||||
Homebrew core does not currently ship `libgpod`, but Strawberry can optionally use it
|
|
||||||
to support **classic iPod** devices (via `libgpod-1.0` + `gdk-pixbuf-2.0`).
|
|
||||||
|
|
||||||
This formula is pinned to a known-good upstream snapshot and disables Linux-specific
|
|
||||||
integration (udev/HAL) and language bindings to keep the build reliable on macOS.
|
|
||||||
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
class Macdeploycheck < Formula
|
|
||||||
desc "Sanity checks a macOS .app bundle for accidental Homebrew runtime dependencies"
|
|
||||||
homepage "https://github.com/strawberrymusicplayer/strawberry"
|
|
||||||
version "0.1.0"
|
|
||||||
# Homebrew requires a URL stanza. Use the script shipped in this tap (file://),
|
|
||||||
# so installs always match the tapped revision.
|
|
||||||
url "file://#{File.expand_path("../dist/macos/macdeploycheck.sh", __dir__)}"
|
|
||||||
sha256 "07d361dcecf98af44fa076cc4253af907e23dee273c198a60128dae41b98432d"
|
|
||||||
license "MIT"
|
|
||||||
|
|
||||||
depends_on :macos
|
|
||||||
|
|
||||||
def install
|
|
||||||
bin.install "macdeploycheck.sh" => "macdeploycheck"
|
|
||||||
end
|
|
||||||
|
|
||||||
test do
|
|
||||||
# Basic smoke test: tool runs and prints usage.
|
|
||||||
system bin/"macdeploycheck"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
# `macdeploycheck` (local Homebrew formula)
|
|
||||||
|
|
||||||
This repository includes a small helper tool called `macdeploycheck`, packaged as a local Homebrew formula.
|
|
||||||
|
|
||||||
## What it does
|
|
||||||
|
|
||||||
`macdeploycheck` scans a built `.app` bundle and flags common **accidental runtime dependencies** on:
|
|
||||||
|
|
||||||
- Homebrew paths like `/opt/homebrew/...` or `/usr/local/...`
|
|
||||||
- MacPorts paths like `/opt/local/...`
|
|
||||||
|
|
||||||
These dependencies usually mean the `.app` is **not self-contained** and may fail to run on other machines or fail notarization validation.
|
|
||||||
|
|
||||||
## Install (via this repo's tap)
|
|
||||||
|
|
||||||
From the Strawberry repo root:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
brew tap strawberry/local "file://$PWD"
|
|
||||||
brew install strawberry/local/macdeploycheck
|
|
||||||
```
|
|
||||||
|
|
||||||
Or use the repo `Brewfile`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
brew bundle --file Brewfile
|
|
||||||
```
|
|
||||||
|
|
||||||
## Use
|
|
||||||
|
|
||||||
```bash
|
|
||||||
macdeploycheck /path/to/Strawberry.app
|
|
||||||
```
|
|
||||||
|
|
||||||
It exits non-zero if it finds external runtime deps.
|
|
||||||
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
class QtsparkleQt6 < Formula
|
|
||||||
desc "Qt wrapper library for in-app updates (Qt 6 build)"
|
|
||||||
homepage "https://github.com/strawberrymusicplayer/qtsparkle"
|
|
||||||
url "https://github.com/strawberrymusicplayer/qtsparkle/archive/95ca3b77a79540d632b29e9a4df9aed30af5f901.tar.gz"
|
|
||||||
sha256 "945c9e96d2f6175b134a8ccfd6ec1acd268266d31969b5870d4037e8e5877834"
|
|
||||||
license "GPL-3.0-or-later"
|
|
||||||
|
|
||||||
depends_on "cmake" => :build
|
|
||||||
depends_on "ninja" => :build
|
|
||||||
depends_on "qt"
|
|
||||||
|
|
||||||
def install
|
|
||||||
args = std_cmake_args + %W[
|
|
||||||
-GNinja
|
|
||||||
-DBUILD_WITH_QT6=ON
|
|
||||||
-DBUILD_WITH_QT5=OFF
|
|
||||||
-DBUILD_SHARED_LIBS=ON
|
|
||||||
-DBUILD_STATIC_LIBS=OFF
|
|
||||||
]
|
|
||||||
|
|
||||||
system "cmake", "-S", ".", "-B", "build", *args
|
|
||||||
system "cmake", "--build", "build"
|
|
||||||
system "cmake", "--install", "build"
|
|
||||||
end
|
|
||||||
|
|
||||||
test do
|
|
||||||
# Strawberry expects: find_package(qtsparkle-qt6) and target qtsparkle-qt6::qtsparkle
|
|
||||||
(testpath/"CMakeLists.txt").write <<~CMAKE
|
|
||||||
cmake_minimum_required(VERSION 3.16)
|
|
||||||
project(qtsparkle_test LANGUAGES CXX)
|
|
||||||
find_package(qtsparkle-qt6 CONFIG REQUIRED)
|
|
||||||
add_library(dummy STATIC dummy.cpp)
|
|
||||||
target_link_libraries(dummy PRIVATE qtsparkle-qt6::qtsparkle)
|
|
||||||
CMAKE
|
|
||||||
|
|
||||||
(testpath/"dummy.cpp").write <<~CPP
|
|
||||||
int dummy() { return 0; }
|
|
||||||
CPP
|
|
||||||
|
|
||||||
system "cmake", "-S", ".", "-B", "build",
|
|
||||||
"-DCMAKE_PREFIX_PATH=#{opt_prefix}"
|
|
||||||
system "cmake", "--build", "build"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
# qtsparkle-qt6 (local Homebrew formula)
|
|
||||||
|
|
||||||
This installs Strawberry’s Qt updater helper library as a CMake package:
|
|
||||||
|
|
||||||
- `find_package(qtsparkle-qt6 CONFIG REQUIRED)`
|
|
||||||
- target: `qtsparkle-qt6::qtsparkle`
|
|
||||||
|
|
||||||
Strawberry will pick it up automatically when present and enable the optional
|
|
||||||
**QtSparkle integration**.
|
|
||||||
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
class SparkleFramework < Formula
|
|
||||||
desc "Sparkle.framework for macOS app updates (framework-only packaging)"
|
|
||||||
homepage "https://sparkle-project.org/"
|
|
||||||
url "https://github.com/sparkle-project/Sparkle/releases/download/2.8.1/Sparkle-2.8.1.tar.xz"
|
|
||||||
sha256 "5cddb7695674ef7704268f38eccaee80e3accbf19e61c1689efff5b6116d85be"
|
|
||||||
license "MIT"
|
|
||||||
|
|
||||||
depends_on :macos
|
|
||||||
|
|
||||||
def install
|
|
||||||
frameworks = prefix/"Frameworks"
|
|
||||||
frameworks.install "Sparkle.framework"
|
|
||||||
end
|
|
||||||
|
|
||||||
test do
|
|
||||||
assert_predicate prefix/"Frameworks/Sparkle.framework", :exist?
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
# sparkle-framework (local Homebrew formula)
|
|
||||||
|
|
||||||
Installs the upstream `Sparkle.framework` into:
|
|
||||||
|
|
||||||
- `$(brew --prefix sparkle-framework)/Frameworks/Sparkle.framework`
|
|
||||||
|
|
||||||
This is used to enable Strawberry’s optional **Sparkle integration** on macOS
|
|
||||||
(`find_library(SPARKLE Sparkle)` in the main `CMakeLists.txt`).
|
|
||||||
|
|
||||||
185
README.md
185
README.md
@@ -1,127 +1,122 @@
|
|||||||
# Strawberry (macOS-focused fork)
|
:strawberry: Strawberry Music Player [](https://github.com/strawberrymusicplayer/strawberry/actions)
|
||||||
|
=======================
|
||||||
|
[](https://github.com/sponsors/jonaski)
|
||||||
|
[](https://patreon.com/jonaskvinge)
|
||||||
|
[](https://paypal.me/jonaskvinge)
|
||||||
|
|
||||||
This repository is a **macOS-focused fork** of upstream Strawberry.
|
Strawberry is a music player and music collection organizer. It is a fork of Clementine released in 2018 aimed at music collectors and audiophiles. It's written in C++ using the Qt toolkit.
|
||||||
|
|
||||||
The goal of this fork is to make Strawberry **build cleanly and repeatably on macOS**, with:
|

|
||||||
|
|
||||||
- Homebrew dependency installation via `Brewfile`
|
Resources:
|
||||||
- local Homebrew formulas (tap) for missing dependencies
|
|
||||||
- build / deploy / signing / notarization helper scripts under `build_tools/`
|
|
||||||
- Sparkle feed configuration knobs so you can publish your own updates
|
|
||||||
|
|
||||||
## Upstream vs this fork (macOS distribution)
|
* Website: https://www.strawberrymusicplayer.org/
|
||||||
|
* Wiki: https://wiki.strawberrymusicplayer.org/
|
||||||
|
* Forum: https://forum.strawberrymusicplayer.org/
|
||||||
|
* Github: https://github.com/strawberrymusicplayer/strawberry
|
||||||
|
* Latest builds: https://builds.strawberrymusicplayer.org/
|
||||||
|
* openSUSE buildservice: https://build.opensuse.org/package/show/home:jonaski:audio/strawberry
|
||||||
|
* Ubuntu PPA: https://launchpad.net/~jonaski/+archive/ubuntu/strawberry
|
||||||
|
* Ubuntu Unstable PPA: https://launchpad.net/~jonaski/+archive/ubuntu/strawberry-unstable
|
||||||
|
* Translations: https://crowdin.com/project/strawberrymusicplayer/
|
||||||
|
|
||||||
Upstream Strawberry is where ongoing development happens:
|
### :bangbang: Opening an issue
|
||||||
|
|
||||||
- Upstream: `https://github.com/strawberrymusicplayer/strawberry`
|
* Read the FAQ: https://wiki.strawberrymusicplayer.org/wiki/FAQ
|
||||||
|
* Search for the issue to see if it is already solved, or if there is an open issue for it already. If there is an open issue already, you can comment on it if you have additional information that could be useful to us.
|
||||||
|
* For technical problems, discussion, questions and feature suggestions use the forum (https://forum.strawberrymusicplayer.org/) instead. The forum is better suited for discussion.
|
||||||
|
* We do not take feature requests from users on GitHub. Any issues related to feature requests will be closed. This does not necessarily mean that we won't add new features, but we don't have time to take feature requests or answer questions about new features from users. It is still possible to suggest or discuss new features on the forum (https://forum.strawberrymusicplayer.org/).
|
||||||
|
* We do not maintain the Flatpak package. Do not report issues related to Flatpak unless the issue can be reproduced with a native package, use Flatpak support instead https://flatpak.org/about/
|
||||||
|
|
||||||
This fork’s source (the code you are building here):
|
### :moneybag: Sponsoring
|
||||||
|
|
||||||
- Fork: `https://gitea.dryark.com/dryark/strawberry`
|
The program is free software, released under GPL. If you like this program and can make use of it, consider sponsoring or donating to help fund the project.
|
||||||
|
There are currently 4 options for sponsoring:
|
||||||
|
|
||||||
This fork is intended for people who want to:
|
1. [GitHub](https://github.com/sponsors/jonaski)
|
||||||
|
2. [Patreon](https://www.patreon.com/jonaskvinge)
|
||||||
|
3. [Ko-fi](https://ko-fi.com/jonaskvinge)
|
||||||
|
4. [PayPal](https://paypal.me/jonaskvinge)
|
||||||
|
|
||||||
- **build from source on macOS** without guesswork
|
Funding developers is a way to contribute to open source projects you appreciate, it helps developers get the resources they need, and recognize contributors working behind the scenes to make open source better for everyone.
|
||||||
- **produce signed + notarized binaries** themselves (and optionally distribute them)
|
|
||||||
|
|
||||||
General safety note: whether you use upstream builds, your own builds, or someone else’s, only install software from sources you trust and prefer **signed + notarized** releases.
|
### :heavy_check_mark: Features
|
||||||
|
|
||||||
## Quick start (macOS)
|
* Play and organize music
|
||||||
|
* Supports WAV, FLAC, WavPack, Ogg FLAC, Ogg Vorbis, Ogg Opus, Ogg Speex, MPC, TrueAudio, AIFF, MP4, MP3, ASF and Monkey's Audio.
|
||||||
|
* Audio CD playback
|
||||||
|
* Native desktop notifications
|
||||||
|
* Playlist management
|
||||||
|
* Smart and dynamic playlists
|
||||||
|
* Advanced audio output and device configuration for bit-perfect playback on Linux
|
||||||
|
* In-player song loudness analysis and song playback loudness normalization, as per EBU R 128
|
||||||
|
* Edit tags on audio files
|
||||||
|
* Fetch tags from MusicBrainz
|
||||||
|
* Album cover art from [Last.fm](https://www.last.fm/), [Musicbrainz](https://musicbrainz.org/), [Discogs](https://www.discogs.com/), [Musixmatch](https://www.musixmatch.com/), [Deezer](https://www.deezer.com/), [Tidal](https://www.tidal.com/), [Qobuz](https://www.qobuz.com/) and [Spotify](https://www.spotify.com/)
|
||||||
|
* Song lyrics from [Genius](https://genius.com/), [Musixmatch](https://www.musixmatch.com/), [ChartLyrics](http://www.chartlyrics.com/), [lyrics.ovh](https://lyrics.ovh/), [lololyrics.com](https://www.lololyrics.com/), [songlyrics.com](https://www.songlyrics.com/), [azlyrics.com](https://www.azlyrics.com/) and [elyrics.net](https://www.elyrics.net/)
|
||||||
|
* Support for multiple backends
|
||||||
|
* Audio analyzer
|
||||||
|
* Audio equalizer
|
||||||
|
* Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
|
||||||
|
* Scrobbler with support for [Last.fm](https://www.last.fm/), [Libre.fm](https://libre.fm/) and [ListenBrainz](https://listenbrainz.org/)
|
||||||
|
* Subsonic, Tidal, Spotify and Qobuz streaming support
|
||||||
|
|
||||||
Install Homebrew dependencies:
|
|
||||||
|
|
||||||
```bash
|
It has so far been tested to work on Linux, OpenBSD, FreeBSD, macOS and Windows.
|
||||||
./build_tools/macos/install_brew_deps.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Build:
|
**Access to macOS and Windows releases are currently restricted to sponsors, a 5 USD monthly sponsorship is required. You can sponsor strawberry through <a href="https://www.patreon.com/jonaskvinge">Patreon</a> for direct access to new releases. If you are sponsoring through GitHub, Ko-fi or PayPal, please e-mail support AT strawberrymusicplayer.org for access to downloads.**
|
||||||
|
|
||||||
```bash
|
### :heavy_exclamation_mark: Requirements
|
||||||
./build_tools/macos/build_app.sh --release --clean
|
|
||||||
open ./cmake-build-macos-release/strawberry.app
|
|
||||||
```
|
|
||||||
|
|
||||||
Build + deploy + sign + notarize (+ DMG):
|
To build Strawberry from source you need the following installed on your system with the additional development packages/headers:
|
||||||
|
|
||||||
```bash
|
* [CMake](https://cmake.org/)
|
||||||
./build_tools/macos/build_sign_notarize.sh --run --release --clean --deploy --dmg \
|
* C/C++ compiler ([GCC](https://gcc.gnu.org/), [Clang](https://clang.llvm.org/) or [MSVC](https://visualstudio.microsoft.com/vs/features/cplusplus/))
|
||||||
--identity "Developer ID Application: Your Name (TEAMID)" \
|
* [pkg-config](https://www.freedesktop.org/wiki/Software/pkg-config/) or [pkgconf](https://github.com/pkgconf/pkgconf)
|
||||||
--notary-profile "<profile-name>"
|
* [Boost](https://www.boost.org/)
|
||||||
```
|
* [GLib](https://developer.gnome.org/glib/)
|
||||||
|
* [Qt 6 or Qt 5.12 or higher with components Core, Gui, Widgets, Concurrent, Network and Sql](https://www.qt.io/)
|
||||||
|
* [SQLite 3.9 or newer](https://www.sqlite.org)
|
||||||
|
* [Protobuf](https://developers.google.com/protocol-buffers/)
|
||||||
|
* [ALSA (Required on Linux)](https://www.alsa-project.org/)
|
||||||
|
* [D-Bus (Required on Linux)](https://www.freedesktop.org/wiki/Software/dbus/)
|
||||||
|
* [GStreamer](https://gstreamer.freedesktop.org/) or [VLC](https://www.videolan.org)
|
||||||
|
* [TagLib 1.11.1 or higher](https://www.taglib.org/) or [TagParser](https://github.com/Martchus/tagparser)
|
||||||
|
* [ICU](https://unicode-org.github.io/icu/)
|
||||||
|
|
||||||
## Features
|
Optional dependencies:
|
||||||
|
|
||||||
- Play and organize your music collection
|
* Song fingerprinting and MusicBrainz tagging: [Chromaprint](https://acoustid.org/chromaprint)
|
||||||
- Supports formats: WAV, FLAC, WavPack, Ogg Vorbis, Opus, MPC, TrueAudio, AIFF, MP4, MP3, ASF, and Monkey’s Audio
|
* Moodbar: [fftw3](http://www.fftw.org/)
|
||||||
- Audio CD playback
|
* PulseAudio integration: [PulseAudio](https://www.freedesktop.org/wiki/Software/PulseAudio/?)
|
||||||
- Bit-perfect playback on Linux
|
* Audio CD: [libcdio](https://www.gnu.org/software/libcdio/)
|
||||||
- Native desktop notifications
|
* MTP devices: [libmtp](http://libmtp.sourceforge.net/)
|
||||||
- Advanced playlist management
|
* iPod Classic devices: [libgpod](http://www.gtkpod.org/libgpod/)
|
||||||
- Smart and dynamic playlists
|
* EBU R 128 loudness normalization [libebur128](https://github.com/jiixyj/libebur128)
|
||||||
- Loudness analysis and EBU R128 normalization
|
|
||||||
- Editing tags and fetching missing tags via [MusicBrainz](https://musicbrainz.org/)
|
|
||||||
- Album art from: [Last.fm](https://www.last.fm/), [MusicBrainz](https://musicbrainz.org/), [Discogs](https://www.discogs.com/), [Musixmatch](https://www.musixmatch.com/), [Deezer](https://www.deezer.com/), [Tidal](https://www.tidal.com/), [Qobuz](https://www.qobuz.com/), [Spotify](https://www.spotify.com/)
|
|
||||||
- Lyrics from: [Genius](https://genius.com/), [Musixmatch](https://www.musixmatch.com/), [ChartLyrics](http://www.chartlyrics.com/), [lyrics.ovh](https://lyrics.ovh/), [lololyrics](https://www.lololyrics.com/), [songlyrics](https://www.songlyrics.com/), [azlyrics](https://www.azlyrics.com/), [elyrics](https://www.elyrics.net/), [letras](https://www.letras.mus.br), [LyricFind](https://lyrics.lyricfind.com) and [lrclib.net](https://lrclib.net/)
|
|
||||||
- Audio analyzer and equalizer
|
|
||||||
- Transfer music to USB, MTP and iPod devices
|
|
||||||
- Scrobbling to [Last.fm](https://www.last.fm/) and [ListenBrainz](https://listenbrainz.org/)
|
|
||||||
- Streaming from Subsonic-compatible servers
|
|
||||||
- Unofficial integrations: Tidal, Spotify, and Qobuz
|
|
||||||
- Discord Rich Presence
|
|
||||||
|
|
||||||
---
|
You should also install the gstreamer plugins base and good, and optionally bad, ugly and libav to support all audio formats.
|
||||||
|
|
||||||
:white_check_mark: Tested on **Linux**, **OpenBSD**, **FreeBSD**, **macOS**, and **Windows**.
|
### :wrench: Compiling from source
|
||||||
|
|
||||||
## :gear: Requirements
|
### Get the code:
|
||||||
|
|
||||||
To build Strawberry from source, you’ll need:
|
git clone --recursive https://github.com/strawberrymusicplayer/strawberry
|
||||||
|
|
||||||
**Dependencies:**
|
### Compile and install:
|
||||||
- [CMake ≥= 3.13](https://cmake.org/)
|
|
||||||
- C/C++ compiler ([GCC](https://gcc.gnu.org/), [Clang](https://clang.llvm.org/), or [MSVC](https://visualstudio.microsoft.com/vs/features/cplusplus/))
|
|
||||||
- [pkg-config](https://www.freedesktop.org/wiki/Software/pkg-config/) or [pkgconf](https://github.com/pkgconf/pkgconf)
|
|
||||||
- [Boost](https://www.boost.org/)
|
|
||||||
- [GLib](https://developer.gnome.org/glib/)
|
|
||||||
- [Qt ≥= 6.4](https://www.qt.io/) (Core, Concurrent, Gui, Widgets, Network, SQL, D-Bus)
|
|
||||||
- [SQLite ≥= 3.9](https://www.sqlite.org)
|
|
||||||
- [ALSA (Linux only)](https://www.alsa-project.org/)
|
|
||||||
- [GStreamer](https://gstreamer.freedesktop.org/)
|
|
||||||
- [TagLib ≥= 1.12](https://www.taglib.org/)
|
|
||||||
- [ICU](https://unicode-org.github.io/icu/)
|
|
||||||
- [KDSingleApplication ≥= 1.1.0](https://github.com/KDAB/KDSingleApplication)
|
|
||||||
|
|
||||||
**Dependencies for optional features:**
|
|
||||||
- Fingerprinting & tagging: [Chromaprint](https://acoustid.org/chromaprint)
|
|
||||||
- Moodbar: [FFTW3](http://www.fftw.org/)
|
|
||||||
- PulseAudio integration: [PulseAudio](https://www.freedesktop.org/wiki/Software/PulseAudio/)
|
|
||||||
- Audio CD support: [libcdio](https://www.gnu.org/software/libcdio/)
|
|
||||||
- MTP devices: [libmtp](http://libmtp.sourceforge.net/)
|
|
||||||
- iPod Classic: [libgpod](http://www.gtkpod.org/libgpod/)
|
|
||||||
- EBU R128 normalization: [libebur128](https://github.com/jiixyj/libebur128)
|
|
||||||
- Discord presence: [RapidJSON](https://rapidjson.org/)
|
|
||||||
|
|
||||||
Also install GStreamer plugins **base**, **good**, and optionally **bad**, **ugly** and **libav** for full codec support.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## :wrench: Build from Source
|
|
||||||
|
|
||||||
**Get the code (this fork):**
|
|
||||||
|
|
||||||
git clone --recursive https://gitea.dryark.com/dryark/strawberry
|
|
||||||
|
|
||||||
**Build and install:**
|
|
||||||
|
|
||||||
cd strawberry
|
cd strawberry
|
||||||
cmake -S . -B build
|
mkdir build
|
||||||
cmake --build build --parallel $(nproc)
|
cd build
|
||||||
sudo cmake --install build
|
cmake .. -DBUILD_WITH_QT6=ON
|
||||||
|
make -j $(nproc)
|
||||||
|
sudo make install
|
||||||
|
|
||||||
For building on Windows with Visual Studio 2022, see: :point_right: https://github.com/strawberrymusicplayer/strawberry-msvc
|
Strawberry is backwards compatible with Qt 5, to compile with Qt 5 use:
|
||||||
|
|
||||||
---
|
cmake .. -DBUILD_WITH_QT5=ON
|
||||||
|
|
||||||
## :package: Packaging status
|
To compile on Windows with Visual Studio 2019 or 2022, see https://github.com/strawberrymusicplayer/strawberry-msvc
|
||||||
|
|
||||||
|
### :penguin: Packaging status
|
||||||
|
|
||||||
[](https://repology.org/metapackage/strawberry/versions)
|
[](https://repology.org/metapackage/strawberry/versions)
|
||||||
|
|||||||
@@ -1,124 +0,0 @@
|
|||||||
# Build helper scripts
|
|
||||||
|
|
||||||
This `build_tools/` directory contains **helper scripts and notes** for building Strawberry.
|
|
||||||
|
|
||||||
- It is **not** intended to be your CMake build output directory.
|
|
||||||
- Recommended CMake build output directories: `cmake-build/`, `build-release/`, etc.
|
|
||||||
|
|
||||||
## macOS
|
|
||||||
|
|
||||||
- Install dependencies via Homebrew:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./build_tools/macos/install_brew_deps.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
- Build Strawberry:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./build_tools/macos/build_app.sh --release
|
|
||||||
open ./cmake-build-macos-release/strawberry.app
|
|
||||||
```
|
|
||||||
|
|
||||||
## macOS signing + notarization (Developer ID distribution)
|
|
||||||
|
|
||||||
This repo includes `build_tools/macos/build_sign_notarize.sh` to automate:
|
|
||||||
|
|
||||||
- build → (optional deploy) → codesign → notarize → staple → verify
|
|
||||||
|
|
||||||
### One-time setup (Apple Developer)
|
|
||||||
|
|
||||||
- **Install certificates**:
|
|
||||||
- In the Apple Developer portal, create (or download) a **Developer ID Application** certificate.
|
|
||||||
- Install it into your login keychain (Xcode can manage this via **Xcode → Settings → Accounts**).
|
|
||||||
|
|
||||||
- **Provisioning profiles**:
|
|
||||||
- For **Developer ID distribution (outside the Mac App Store)**, you typically **do not need a provisioning profile**.
|
|
||||||
- You *do* need profiles if you are building a **Mac App Store**-signed app (not what this repo’s scripts target).
|
|
||||||
|
|
||||||
- **Notarization credentials**:
|
|
||||||
- Create a `notarytool` keychain profile (recommended) so you don’t have to pass secrets on the command line:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# NOTE: <profile-name> is a positional argument (not a flag).
|
|
||||||
# Pick any name you want, e.g. "strawberry-notary".
|
|
||||||
xcrun notarytool store-credentials "<profile-name>" \
|
|
||||||
--apple-id "<your-apple-id>" \
|
|
||||||
--team-id "<TEAMID>" \
|
|
||||||
--password "<app-specific-password>"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Listing what’s installed locally
|
|
||||||
|
|
||||||
Run with no args to list local signing identities + notarytool profiles:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./build_tools/macos/build_sign_notarize.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### Build + sign + notarize
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./build_tools/macos/build_sign_notarize.sh --run --release --clean --deploy \
|
|
||||||
--identity "Developer ID Application: Your Name (TEAMID)" \
|
|
||||||
--notary-profile "<profile-name>"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Build + sign + notarize + DMG (recommended for public distribution)
|
|
||||||
|
|
||||||
This produces:
|
|
||||||
|
|
||||||
- a notarized `strawberry.app` (stapled)
|
|
||||||
- a notarized `strawberry-notarize.zip` (useful for Sparkle / downloads)
|
|
||||||
- a notarized `strawberry-*.dmg` (stapled)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./build_tools/macos/build_sign_notarize.sh --run --release --clean --deploy --dmg \
|
|
||||||
--identity "Developer ID Application: Your Name (TEAMID)" \
|
|
||||||
--notary-profile "<profile-name>"
|
|
||||||
```
|
|
||||||
|
|
||||||
## macOS Mac App Store (MAS) build + signed PKG
|
|
||||||
|
|
||||||
This repo includes `build_tools/macos/build_mas_pkg.sh` to automate:
|
|
||||||
|
|
||||||
- build (MAS mode) → deploy (bundle deps) → embed provisioning profile → codesign → `productbuild` a signed `.pkg`
|
|
||||||
|
|
||||||
### Requirements (Apple Developer)
|
|
||||||
|
|
||||||
- An App Store Connect app record with bundle id **`com.dryark.strawberry`** (or your own).
|
|
||||||
- A **Mac App Store provisioning profile** for that App ID.
|
|
||||||
- Signing identities installed in your Keychain:
|
|
||||||
- **Apple Distribution** (for the `.app`)
|
|
||||||
- **3rd Party Mac Developer Installer** (for the `.pkg`)
|
|
||||||
|
|
||||||
Tip: list what you have installed:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
security find-identity -p codesigning -v
|
|
||||||
security find-identity -p basic -v
|
|
||||||
ls -la "$HOME/Library/MobileDevice/Provisioning Profiles" | head -n 50
|
|
||||||
```
|
|
||||||
|
|
||||||
### Manual setup guide (certificates, Keychain Access, profiles)
|
|
||||||
|
|
||||||
See: `build_tools/macos/README_MAS.md`
|
|
||||||
|
|
||||||
### Build the signed upload PKG
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./build_tools/macos/build_mas_pkg.sh --run --release --clean \
|
|
||||||
--codesign-identity "Apple Distribution: Your Name (TEAMID)" \
|
|
||||||
--installer-identity "3rd Party Mac Developer Installer: Your Name (TEAMID)" \
|
|
||||||
--provisionprofile "$HOME/Library/MobileDevice/Provisioning Profiles/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX.provisionprofile"
|
|
||||||
```
|
|
||||||
|
|
||||||
Output:
|
|
||||||
|
|
||||||
- `cmake-build-macos-release-mas/strawberry.app`
|
|
||||||
- `cmake-build-macos-release-mas/strawberry-mas.pkg`
|
|
||||||
|
|
||||||
### Upload + submit for review
|
|
||||||
|
|
||||||
- Upload the `.pkg` using Apple’s **Transporter** app (App Store Connect), or with `iTMSTransporter`.
|
|
||||||
- In App Store Connect, wait for processing, select the build, then **Submit for Review**.
|
|
||||||
@@ -1,421 +0,0 @@
|
|||||||
# Mac App Store (MAS) submission guide (manual steps)
|
|
||||||
|
|
||||||
This repo supports a **Mac App Store build mode** (`BUILD_FOR_MAC_APP_STORE=ON`) and includes scripts to build a signed upload `.pkg`.
|
|
||||||
|
|
||||||
If you’re blocked because `security find-identity` only shows **Developer ID** and not **Apple Distribution / Installer**, follow the steps below.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Open Keychain Access (macOS “hidden” Utilities)
|
|
||||||
|
|
||||||
Any of these work:
|
|
||||||
|
|
||||||
- **Spotlight**: press `⌘ + Space` → type **Keychain Access** → Enter
|
|
||||||
- **Finder**: Applications → Utilities → **Keychain Access**
|
|
||||||
- **Terminal**:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
open -a "Keychain Access"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## The core issue: certificate exists but is not a usable identity
|
|
||||||
|
|
||||||
If you see certificates like:
|
|
||||||
|
|
||||||
- `Apple Distribution: ...`
|
|
||||||
- `3rd Party Mac Developer Installer: ...`
|
|
||||||
|
|
||||||
but `security find-identity` does **not** list them, then the certificate is present but **the private key is missing** (or not paired / in the wrong keychain).
|
|
||||||
|
|
||||||
You can confirm with:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./build_tools/macos/check_signing_identities.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 1 — Create the private keys on this Mac (CSR)
|
|
||||||
|
|
||||||
1. Open **Keychain Access**
|
|
||||||
2. Menu: **Keychain Access → Certificate Assistant → Request a Certificate From a Certificate Authority…**
|
|
||||||
3. Fill:
|
|
||||||
- **User Email Address**: your Apple ID email
|
|
||||||
- **Common Name**: e.g. `Dry Ark LLC` (any label is fine)
|
|
||||||
- **CA Email Address**: leave blank
|
|
||||||
- Select: **Saved to disk**
|
|
||||||
4. Save the CSR (`.certSigningRequest`) somewhere safe
|
|
||||||
|
|
||||||
This CSR step is what creates the **private key** locally in your login keychain.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 2 — Create + download the certificates (Apple Developer portal)
|
|
||||||
|
|
||||||
In Apple Developer → **Certificates, Identifiers & Profiles** → **Certificates** → **+**:
|
|
||||||
|
|
||||||
- Create **Apple Distribution** (use the CSR you just made)
|
|
||||||
- Create **Mac Installer Distribution** (or “3rd Party Mac Developer Installer”, wording varies) (use a CSR)
|
|
||||||
|
|
||||||
Download the resulting `.cer` files.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 3 — Install certificates into your login keychain
|
|
||||||
|
|
||||||
Double-click each downloaded `.cer` to install it.
|
|
||||||
|
|
||||||
Then in **Keychain Access → login → My Certificates**:
|
|
||||||
|
|
||||||
- Find **Apple Distribution: ...** and **expand it**
|
|
||||||
- You must see a **private key** under it.
|
|
||||||
- Find **... Installer ...** and expand it
|
|
||||||
- You must see a **private key** under it.
|
|
||||||
|
|
||||||
If there’s no private key under the certificate, it will not be usable for signing on this Mac.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 4 — Verify identities from the CLI
|
|
||||||
|
|
||||||
### Common failure: errSecInternalComponent / chain-to-root warnings
|
|
||||||
|
|
||||||
If you see errors like:
|
|
||||||
|
|
||||||
- `Warning: unable to build chain to self-signed root for signer "Apple Distribution: ..."`
|
|
||||||
- `errSecInternalComponent`
|
|
||||||
|
|
||||||
This is almost always a **keychain search list / trust chain** issue.
|
|
||||||
|
|
||||||
#### Important: do NOT “Always Trust” your Apple Distribution / Installer certs
|
|
||||||
|
|
||||||
Setting your leaf signing certificates (e.g. **Apple Distribution** / **3rd Party Mac Developer Installer**) to **Always Trust** can make things worse by overriding the normal trust chain and causing codesign to fail chain building.
|
|
||||||
|
|
||||||
If you changed trust settings:
|
|
||||||
|
|
||||||
- In **Keychain Access → login → My Certificates**
|
|
||||||
- open the cert → **Trust**
|
|
||||||
- set **“When using this certificate” = “Use System Defaults”**
|
|
||||||
|
|
||||||
Fix (safe, common): ensure the System keychains are included in the user search list:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
security list-keychains -d user
|
|
||||||
security list-keychains -d user -s "$HOME/Library/Keychains/login.keychain-db" "/Library/Keychains/System.keychain" "/System/Library/Keychains/SystemRootCertificates.keychain"
|
|
||||||
```
|
|
||||||
|
|
||||||
Then re-run the build/sign script.
|
|
||||||
|
|
||||||
#### Install the correct Apple intermediate certificates (WWDR)
|
|
||||||
|
|
||||||
If the System keychains are already in the search list and you still get chain errors, you’re likely missing an Apple intermediate (commonly **WWDR**).
|
|
||||||
|
|
||||||
Download the current Apple WWDR intermediate certificate(s) from Apple’s official Certificate Authority page:
|
|
||||||
|
|
||||||
- `https://www.apple.com/certificateauthority/`
|
|
||||||
|
|
||||||
Then import into the **System** keychain (recommended):
|
|
||||||
|
|
||||||
- Keychain Access → **System** keychain → File → **Import Items…** → select the downloaded `.cer`
|
|
||||||
|
|
||||||
Or via CLI (requires admin):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo security add-certificates -k /Library/Keychains/System.keychain "/path/to/WWDR.cer"
|
|
||||||
```
|
|
||||||
|
|
||||||
Verify it’s visible:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
security find-certificate -a -c "Apple Worldwide Developer Relations" /Library/Keychains/System.keychain | head -n 10
|
|
||||||
```
|
|
||||||
|
|
||||||
If needed, you can also verify the chain for your distribution cert:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
security verify-cert -c "Apple Distribution: Dry Ark LLC (7628766FL2)" 2>&1 | head -n 80
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
security find-identity -p codesigning -v
|
|
||||||
security find-identity -p basic -v
|
|
||||||
./build_tools/macos/check_signing_identities.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected:
|
|
||||||
|
|
||||||
- `Apple Distribution: ...` shows up under **codesigning**
|
|
||||||
- `... Installer ...` shows up as an **installer identity** (used to sign upload `.pkg`)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 5 — Create + install the provisioning profile (Mac App Store)
|
|
||||||
|
|
||||||
In Apple Developer → **Profiles** → **+**:
|
|
||||||
|
|
||||||
- Platform: **macOS**
|
|
||||||
- Type: **Mac App Store**
|
|
||||||
- App ID: `com.dryark.strawberry` (or your own bundle id)
|
|
||||||
- Select the **Apple Distribution** certificate
|
|
||||||
- Generate + Download
|
|
||||||
|
|
||||||
### Where the `.provisionprofile` ends up (newer Xcode/macOS)
|
|
||||||
|
|
||||||
Recent Xcode versions store “downloaded manual profiles” under:
|
|
||||||
|
|
||||||
- `~/Library/Developer/Xcode/UserData/Provisioning Profiles/`
|
|
||||||
|
|
||||||
Older tooling sometimes used:
|
|
||||||
|
|
||||||
- `~/Library/MobileDevice/Provisioning Profiles/`
|
|
||||||
|
|
||||||
This repo’s MAS build script does **not** require the profile to be in a specific folder — you can pass the path directly.
|
|
||||||
|
|
||||||
To locate and pick the right profile, use:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./build_tools/macos/find_mas_provisioning_profile.sh --bundle-id com.dryark.strawberry
|
|
||||||
```
|
|
||||||
|
|
||||||
### (Optional) Copy to the legacy folder
|
|
||||||
|
|
||||||
If some other tools expect the legacy folder, you can copy it there:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir -p "$HOME/Library/MobileDevice/Provisioning Profiles"
|
|
||||||
cp -f "/path/to/profile.provisionprofile" "$HOME/Library/MobileDevice/Provisioning Profiles/"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 6 — Build the signed upload package (.pkg)
|
|
||||||
|
|
||||||
This repo provides:
|
|
||||||
|
|
||||||
- `build_tools/macos/build_mas_pkg.sh` (build → deploy → embed profile → sign → productbuild)
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./build_tools/macos/build_mas_pkg.sh --run --release --clean \
|
|
||||||
--codesign-identity "Apple Distribution: Dry Ark LLC (7628766FL2)" \
|
|
||||||
--installer-identity "3rd Party Mac Developer Installer: Dry Ark LLC (7628766FL2)" \
|
|
||||||
--provisionprofile "$HOME/Library/MobileDevice/Provisioning Profiles/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX.provisionprofile"
|
|
||||||
```
|
|
||||||
|
|
||||||
Outputs:
|
|
||||||
|
|
||||||
- `cmake-build-macos-release-mas/strawberry.app`
|
|
||||||
- `cmake-build-macos-release-mas/strawberry-mas.pkg`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Architecture note — arm64 vs universal (arm64+x86_64)
|
|
||||||
|
|
||||||
For Mac App Store uploads, your `.pkg` can contain either:
|
|
||||||
|
|
||||||
- **arm64-only** app (Apple Silicon only), or
|
|
||||||
- **universal** app (arm64 + x86_64), or
|
|
||||||
- **x86_64-only** app (runs on Apple Silicon under Rosetta 2, but native Intel only otherwise)
|
|
||||||
|
|
||||||
Apple does **not** require universal binaries for review. **arm64-only is allowed**, but:
|
|
||||||
|
|
||||||
- Intel Macs **cannot** run an arm64-only app.
|
|
||||||
- If you ship arm64-only, App Store Connect will effectively make the app available only to Apple Silicon Macs (and your listing will reflect that).
|
|
||||||
|
|
||||||
### Recommendation
|
|
||||||
|
|
||||||
- If you want the broadest compatibility, aim for a **universal build**.
|
|
||||||
- If you’re okay supporting only Apple Silicon Macs, arm64-only is the simplest path.
|
|
||||||
|
|
||||||
### Can I upload two different `.pkg`s (one arm64, one x86_64)?
|
|
||||||
|
|
||||||
Not in the way you want.
|
|
||||||
|
|
||||||
- In App Store Connect you can upload multiple builds over time, but for any given version/submission you ultimately pick **one build** to submit.
|
|
||||||
- Apple will not “merge” two separate uploads (arm64-only + x86_64-only) into one app for customers.
|
|
||||||
|
|
||||||
If you want both Apple Silicon and Intel supported **natively**, you need to produce a **single universal** app bundle and package that into **one** `.pkg`.
|
|
||||||
|
|
||||||
If you don’t want to deal with universal yet, your practical choices are:
|
|
||||||
|
|
||||||
- **arm64-only**: Apple Silicon only.
|
|
||||||
- **x86_64-only**: runs on Intel natively, and on Apple Silicon under **Rosetta 2** (slower, but widely compatible).
|
|
||||||
|
|
||||||
### Practical reality for this repo
|
|
||||||
|
|
||||||
This project depends on large native dependency stacks (Qt, GStreamer, plugins). If you build those via Homebrew, you typically end up with **single-architecture** libraries (arm64 under `/opt/homebrew`, x86_64 under `/usr/local`).
|
|
||||||
|
|
||||||
A true universal app requires **all bundled native code** (your executable *and* all `.dylib`/plugins/frameworks you ship) to be universal as well.
|
|
||||||
|
|
||||||
If you decide you want universal:
|
|
||||||
|
|
||||||
- You’ll need a universal build of **Qt** and **GStreamer** (and all bundled plugins), or
|
|
||||||
- Build arm64 and x86_64 bundles separately and combine *matching* binaries where possible (advanced; easy to break signing / plugin loading).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Troubleshooting — `productbuild` fails with CSSM `-60008` (authorization)
|
|
||||||
|
|
||||||
If you see something like:
|
|
||||||
|
|
||||||
- `SignData failed ... CSSM Exception: -60008 Unable to obtain authorization for this operation`
|
|
||||||
|
|
||||||
That means the **Installer** certificate is present, but macOS is not allowing `productbuild` to use the **private key** without additional authorization.
|
|
||||||
|
|
||||||
### Fix option A (recommended): set key partition list (CLI)
|
|
||||||
|
|
||||||
This is the standard “allow Apple tools to sign without GUI prompts” fix:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
security unlock-keychain "$HOME/Library/Keychains/login.keychain-db"
|
|
||||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "<login-keychain-password>" "$HOME/Library/Keychains/login.keychain-db"
|
|
||||||
```
|
|
||||||
|
|
||||||
Note: if your password contains characters like `!` or `$` and you paste it into a command in `zsh`,
|
|
||||||
the shell can modify it (history/variable expansion) and `security ... -k` may claim it’s “incorrect”.
|
|
||||||
Use **single quotes** (or the env var path shown below) to avoid this, e.g.:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k 'p@ssw0rd!$' "$HOME/Library/Keychains/login.keychain-db"
|
|
||||||
```
|
|
||||||
|
|
||||||
Then rerun:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./build_tools/macos/build_mas_pkg.sh --run ...
|
|
||||||
```
|
|
||||||
|
|
||||||
This repo’s script also supports:
|
|
||||||
|
|
||||||
- `--keychain-password <pw>` (or env var `STRAWBERRY_KEYCHAIN_PASSWORD`)
|
|
||||||
|
|
||||||
### Fix option B: Keychain Access UI (one-time)
|
|
||||||
|
|
||||||
1. Open **Keychain Access**
|
|
||||||
2. Select **login** keychain → **My Certificates**
|
|
||||||
3. Find your installer cert (e.g. `3rd Party Mac Developer Installer: ...`) and **expand it**
|
|
||||||
4. Select the **private key** under it
|
|
||||||
5. **Get Info → Access Control**
|
|
||||||
- Add `/usr/bin/productbuild` (and optionally `/usr/bin/pkgbuild`) to the allowed apps
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Step 7 — Upload + submit for review
|
|
||||||
|
|
||||||
### 7.1 Install Apple “Transporter” (the upload tool)
|
|
||||||
|
|
||||||
Apple requires Mac App Store submissions to be uploaded using **Transporter** (a macOS app published by Apple).
|
|
||||||
|
|
||||||
Where to get it:
|
|
||||||
|
|
||||||
- Install **Transporter** from the **Mac App Store** (search for “Transporter”).
|
|
||||||
- App Store listing name is typically **“Transporter”** by Apple.
|
|
||||||
|
|
||||||
### 7.2 Upload the `.pkg` with Transporter
|
|
||||||
|
|
||||||
1. Open **Transporter**
|
|
||||||
2. Sign in with the Apple ID that has access to **App Store Connect**
|
|
||||||
3. Click **Add App** (or **+**) and choose your signed upload package:
|
|
||||||
- `cmake-build-macos-release-mas/strawberry-mas.pkg` (or your custom `--pkg-out` path)
|
|
||||||
4. Click **Deliver**
|
|
||||||
5. Wait for upload + server-side validation to complete
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
|
|
||||||
- Uploading can take a while depending on your connection.
|
|
||||||
- If Transporter reports an error, the message usually includes the exact App Store Connect requirement you violated (bundle id mismatch, missing entitlements, invalid signature, etc.).
|
|
||||||
|
|
||||||
### 7.3 Submit the build in App Store Connect
|
|
||||||
|
|
||||||
1. Open **App Store Connect** in your browser and go to **My Apps**
|
|
||||||
2. Select your app, then go to the **macOS App** platform section
|
|
||||||
3. Find your uploaded build under **TestFlight** or **Prepare for Submission** (Apple’s UI wording changes over time)
|
|
||||||
4. Wait for Apple to finish “Processing” the build
|
|
||||||
5. Select the build for your version, complete required metadata, then click **Submit for Review**
|
|
||||||
|
|
||||||
### (Optional) CLI upload (advanced): `iTMSTransporter`
|
|
||||||
|
|
||||||
If you prefer uploading from the command line, Apple’s underlying uploader is **iTMSTransporter**.
|
|
||||||
On most systems it’s available via Xcode command line tools as:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
xcrun iTMSTransporter -help
|
|
||||||
```
|
|
||||||
|
|
||||||
CLI upload requires additional credentials (App Store Connect API key or Apple ID auth) and is easier to get wrong than the Transporter GUI.
|
|
||||||
For most folks, **Transporter.app is the recommended path**.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Creating a universal Mac App Store upload using two Macs (arm64 + x86_64)
|
|
||||||
|
|
||||||
If you have both an Apple Silicon Mac and an Intel Mac, the most reliable way to ship a universal app for this repo is:
|
|
||||||
|
|
||||||
1. Build + deploy the **unsigned** MAS app bundle on each machine (arm64 and x86_64).
|
|
||||||
2. Copy both `.app` bundles to the machine that has your signing keys.
|
|
||||||
3. Merge them with `lipo` and then **sign + package** once, producing a single universal `.pkg`.
|
|
||||||
|
|
||||||
### Step A — Build + deploy (arm64 machine)
|
|
||||||
|
|
||||||
On your Apple Silicon Mac:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./build_tools/macos/build_app.sh --release --clean --mas --deploy --build-dir ./cmake-build-macos-release-mas-arm64
|
|
||||||
```
|
|
||||||
|
|
||||||
This produces (unsigned):
|
|
||||||
|
|
||||||
- `cmake-build-macos-release-mas-arm64/strawberry.app`
|
|
||||||
|
|
||||||
### Step B — Build + deploy (x86_64 machine)
|
|
||||||
|
|
||||||
On your Intel Mac:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./build_tools/macos/build_app.sh --release --clean --mas --deploy --build-dir ./cmake-build-macos-release-mas-x86_64
|
|
||||||
```
|
|
||||||
|
|
||||||
This produces (unsigned):
|
|
||||||
|
|
||||||
- `cmake-build-macos-release-mas-x86_64/strawberry.app`
|
|
||||||
|
|
||||||
### Step C — Copy both app bundles to one “packaging” machine
|
|
||||||
|
|
||||||
Pick the Mac that has your **Apple Distribution** and **Installer** identities (private keys) installed.
|
|
||||||
Copy both `.app` bundles onto that Mac, for example:
|
|
||||||
|
|
||||||
- `/path/to/inputs/strawberry-arm64.app`
|
|
||||||
- `/path/to/inputs/strawberry-x86_64.app`
|
|
||||||
|
|
||||||
Tip: `rsync` works well for app bundles:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
rsync -a "/path/to/arm64/strawberry.app" "/path/to/inputs/strawberry-arm64.app"
|
|
||||||
rsync -a "/path/to/x86_64/strawberry.app" "/path/to/inputs/strawberry-x86_64.app"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step D — Merge + sign + build the universal `.pkg`
|
|
||||||
|
|
||||||
On the packaging machine:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./build_tools/macos/build_mas_universal_pkg.sh --run \
|
|
||||||
--arm-app "/path/to/inputs/strawberry-arm64.app" \
|
|
||||||
--x86-app "/path/to/inputs/strawberry-x86_64.app" \
|
|
||||||
--codesign-identity "Apple Distribution: Your Name (TEAMID)" \
|
|
||||||
--installer-identity "3rd Party Mac Developer Installer: Your Name (TEAMID)" \
|
|
||||||
--provisionprofile "/path/to/profile.provisionprofile"
|
|
||||||
```
|
|
||||||
|
|
||||||
Outputs:
|
|
||||||
|
|
||||||
- `cmake-build-macos-release-mas-universal/strawberry.app` (universal)
|
|
||||||
- `cmake-build-macos-release-mas-universal/strawberry-mas-universal.pkg`
|
|
||||||
|
|
||||||
### Important constraints (don’t skip)
|
|
||||||
|
|
||||||
- The two input apps must be built from the **same commit** with the **same enabled features** so the app bundle layouts match.
|
|
||||||
- Do **not** sign the per-arch apps first; `lipo` invalidates signatures. Sign **only after** merging.
|
|
||||||
@@ -1,277 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
ts() { date +"%H:%M:%S"; }
|
|
||||||
lower() { echo "$1" | tr '[:upper:]' '[:lower:]'; }
|
|
||||||
|
|
||||||
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
repo_root="$(cd -- "${script_dir}/../.." && pwd)"
|
|
||||||
|
|
||||||
current_cmd_pid=""
|
|
||||||
current_hb_pid=""
|
|
||||||
|
|
||||||
kill_tree() {
|
|
||||||
local pid="$1"
|
|
||||||
[[ -z "${pid}" ]] && return 0
|
|
||||||
# Recurse into children first (best-effort).
|
|
||||||
local child
|
|
||||||
for child in $(pgrep -P "$pid" 2>/dev/null || true); do
|
|
||||||
kill_tree "$child"
|
|
||||||
done
|
|
||||||
kill -TERM "$pid" >/dev/null 2>&1 || true
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
# Never fail cleanup on errors.
|
|
||||||
set +e
|
|
||||||
if [[ -n "${current_hb_pid}" ]]; then
|
|
||||||
kill "${current_hb_pid}" >/dev/null 2>&1 || true
|
|
||||||
wait "${current_hb_pid}" >/dev/null 2>&1 || true
|
|
||||||
current_hb_pid=""
|
|
||||||
fi
|
|
||||||
if [[ -n "${current_cmd_pid}" ]]; then
|
|
||||||
# If still running, terminate process tree.
|
|
||||||
kill -0 "${current_cmd_pid}" >/dev/null 2>&1 && kill_tree "${current_cmd_pid}"
|
|
||||||
current_cmd_pid=""
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
trap 'cleanup; exit 130' INT TERM
|
|
||||||
trap 'cleanup' EXIT
|
|
||||||
|
|
||||||
run_with_heartbeat() {
|
|
||||||
local desc="$1"
|
|
||||||
shift
|
|
||||||
|
|
||||||
local start now elapsed hb_pid
|
|
||||||
start="$(date +%s)"
|
|
||||||
|
|
||||||
echo "==> [$(ts)] ${desc}"
|
|
||||||
|
|
||||||
# Run the command in the background so we can reliably clean it up on Ctrl-C.
|
|
||||||
set +e
|
|
||||||
"$@" &
|
|
||||||
local cmd_pid=$!
|
|
||||||
set -e
|
|
||||||
current_cmd_pid="$cmd_pid"
|
|
||||||
|
|
||||||
(
|
|
||||||
while kill -0 "$cmd_pid" >/dev/null 2>&1; do
|
|
||||||
sleep 20
|
|
||||||
now="$(date +%s)"
|
|
||||||
elapsed="$((now - start))"
|
|
||||||
echo " [$(ts)] ... still working (${elapsed}s elapsed) ..."
|
|
||||||
done
|
|
||||||
) &
|
|
||||||
hb_pid="$!"
|
|
||||||
current_hb_pid="$hb_pid"
|
|
||||||
|
|
||||||
set +e
|
|
||||||
wait "$cmd_pid"
|
|
||||||
local rc=$?
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Clear globals before stopping heartbeat to avoid cleanup double-kill.
|
|
||||||
current_cmd_pid=""
|
|
||||||
kill "$hb_pid" >/dev/null 2>&1 || true
|
|
||||||
wait "$hb_pid" >/dev/null 2>&1 || true
|
|
||||||
current_hb_pid=""
|
|
||||||
|
|
||||||
now="$(date +%s)"
|
|
||||||
elapsed="$((now - start))"
|
|
||||||
|
|
||||||
if [[ $rc -ne 0 ]]; then
|
|
||||||
echo "Error: '${desc}' failed after ${elapsed}s (exit $rc)." >&2
|
|
||||||
return "$rc"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Done: ${desc} (${elapsed}s)"
|
|
||||||
}
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'EOF'
|
|
||||||
Usage:
|
|
||||||
./build_tools/macos/build_app.sh [--debug|--release] [--mas] [--deploy] [--dmg] [--clean] [--build-dir <path>]
|
|
||||||
|
|
||||||
What it does:
|
|
||||||
- Configures and builds Strawberry with CMake + Ninja
|
|
||||||
- Optional: runs CMake targets 'deploy' (bundle deps) and 'dmg' (create DMG)
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--release Release build (default)
|
|
||||||
--debug Debug build
|
|
||||||
--mas Build for Mac App Store (BUILD_FOR_MAC_APP_STORE=ON). Disables Sparkle/QtSparkle and any localhost OAuth redirect listener.
|
|
||||||
--deploy Run: cmake --build <builddir> --target deploy
|
|
||||||
--dmg Run: cmake --build <builddir> --target dmg (implies --deploy)
|
|
||||||
--clean Delete the build dir before configuring
|
|
||||||
--build-dir Override build directory (default: <repo>/cmake-build-macos-<config>)
|
|
||||||
-h, --help Show help
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
config="Release"
|
|
||||||
do_mas=0
|
|
||||||
do_deploy=0
|
|
||||||
do_dmg=0
|
|
||||||
do_clean=0
|
|
||||||
build_dir=""
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--release) config="Release"; shift ;;
|
|
||||||
--debug) config="Debug"; shift ;;
|
|
||||||
--mas) do_mas=1; shift ;;
|
|
||||||
--deploy) do_deploy=1; shift ;;
|
|
||||||
--dmg) do_dmg=1; do_deploy=1; shift ;;
|
|
||||||
--clean) do_clean=1; shift ;;
|
|
||||||
--build-dir) build_dir="${2:-}"; shift 2 ;;
|
|
||||||
-h|--help) usage; exit 0 ;;
|
|
||||||
*) echo "Unknown arg: $1" >&2; usage; exit 2 ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ "$(uname -s)" != "Darwin" ]]; then
|
|
||||||
echo "Error: This script is for macOS only." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! command -v xcode-select >/dev/null 2>&1 || ! xcode-select -p >/dev/null 2>&1; then
|
|
||||||
echo "Error: Xcode Command Line Tools not found." >&2
|
|
||||||
echo "Install them first: xcode-select --install" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! command -v brew >/dev/null 2>&1; then
|
|
||||||
echo "Error: Homebrew ('brew') not found in PATH." >&2
|
|
||||||
echo "Install Homebrew first: https://brew.sh/" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! command -v cmake >/dev/null 2>&1; then
|
|
||||||
echo "Error: cmake not found. Did you run ./build_tools/macos/install_brew_deps.sh ?" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! command -v ninja >/dev/null 2>&1; then
|
|
||||||
echo "Error: ninja not found. Did you run ./build_tools/macos/install_brew_deps.sh ?" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
brew_prefix="$(brew --prefix)"
|
|
||||||
qt_prefix="$(brew --prefix qt)"
|
|
||||||
icu_prefix="$(brew --prefix icu4c || true)"
|
|
||||||
|
|
||||||
if [[ -z "$build_dir" ]]; then
|
|
||||||
build_dir="${repo_root}/cmake-build-macos-$(lower "$config")"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Repo: ${repo_root}"
|
|
||||||
echo "==> [$(ts)] Build dir: ${build_dir}"
|
|
||||||
echo "==> [$(ts)] Config: ${config}"
|
|
||||||
if [[ "$do_mas" -eq 1 ]]; then
|
|
||||||
echo "==> [$(ts)] MAS: enabled (BUILD_FOR_MAC_APP_STORE=ON)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$do_clean" -eq 1 ]]; then
|
|
||||||
echo "==> [$(ts)] Cleaning build dir"
|
|
||||||
# macOS 26+ can apply provenance metadata that blocks deletion even when permissions look normal.
|
|
||||||
# Clear common xattrs and immutable flags before deleting.
|
|
||||||
xattr -dr com.apple.provenance "$build_dir" >/dev/null 2>&1 || true
|
|
||||||
xattr -dr com.apple.quarantine "$build_dir" >/dev/null 2>&1 || true
|
|
||||||
chflags -R nouchg,noschg "$build_dir" >/dev/null 2>&1 || true
|
|
||||||
rm -rf "$build_dir" || {
|
|
||||||
echo "Error: failed to remove build dir: $build_dir" >&2
|
|
||||||
echo "This is usually due to macOS provenance/flags. Try:" >&2
|
|
||||||
echo " xattr -dr com.apple.provenance \"$build_dir\"" >&2
|
|
||||||
echo " chflags -R nouchg,noschg \"$build_dir\"" >&2
|
|
||||||
echo " rm -rf \"$build_dir\"" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
fi
|
|
||||||
|
|
||||||
mkdir -p "$build_dir"
|
|
||||||
|
|
||||||
# If you've run a previously-built app directly from the build directory, macOS can apply provenance
|
|
||||||
# metadata that makes the bundle effectively immutable (even when permissions look normal).
|
|
||||||
# That breaks CMake because it needs to update strawberry.app/Contents/Info.plist during configure/build.
|
|
||||||
app_bundle="${build_dir}/strawberry.app"
|
|
||||||
if [[ -d "${app_bundle}/Contents" ]]; then
|
|
||||||
# Try to clear provenance/quarantine metadata first (best effort).
|
|
||||||
xattr -dr com.apple.provenance "${app_bundle}" >/dev/null 2>&1 || true
|
|
||||||
xattr -dr com.apple.quarantine "${app_bundle}" >/dev/null 2>&1 || true
|
|
||||||
|
|
||||||
# If the bundle is still not writable, remove it so CMake can recreate it.
|
|
||||||
if ! ( : > "${app_bundle}/Contents/.cmake_write_test" ) 2>/dev/null; then
|
|
||||||
echo "==> [$(ts)] Existing ${app_bundle} is not writable (likely macOS provenance). Removing it."
|
|
||||||
rm -rf "${app_bundle}"
|
|
||||||
else
|
|
||||||
rm -f "${app_bundle}/Contents/.cmake_write_test" >/dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Make pkg-config more reliable with Homebrew.
|
|
||||||
export PKG_CONFIG_PATH="${brew_prefix}/lib/pkgconfig:${brew_prefix}/share/pkgconfig:${PKG_CONFIG_PATH:-}"
|
|
||||||
|
|
||||||
# For dist/CMakeLists.txt Info.plist minimum version logic.
|
|
||||||
export MACOSX_DEPLOYMENT_TARGET="${MACOSX_DEPLOYMENT_TARGET:-12.0}"
|
|
||||||
|
|
||||||
cmake_prefix_path="${qt_prefix};${brew_prefix}"
|
|
||||||
|
|
||||||
cmake_extra_args=()
|
|
||||||
|
|
||||||
# Mac App Store build mode
|
|
||||||
if [[ "$do_mas" -eq 1 ]]; then
|
|
||||||
cmake_extra_args+=("-DBUILD_FOR_MAC_APP_STORE=ON")
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Optional: override Sparkle update feed / key for your own published builds.
|
|
||||||
# Example:
|
|
||||||
# export SPARKLE_FEED_URL="https://example.com/appcast.xml"
|
|
||||||
# export SPARKLE_PUBLIC_ED25519_KEY="base64=="
|
|
||||||
if [[ -n "${SPARKLE_FEED_URL:-}" ]]; then
|
|
||||||
cmake_extra_args+=("-DSPARKLE_FEED_URL=${SPARKLE_FEED_URL}")
|
|
||||||
fi
|
|
||||||
if [[ -n "${SPARKLE_PUBLIC_ED25519_KEY:-}" ]]; then
|
|
||||||
cmake_extra_args+=("-DSPARKLE_PUBLIC_ED25519_KEY=${SPARKLE_PUBLIC_ED25519_KEY}")
|
|
||||||
fi
|
|
||||||
|
|
||||||
run_with_heartbeat "Configuring (CMAKE_PREFIX_PATH=${cmake_prefix_path})" \
|
|
||||||
cmake -S "$repo_root" -B "$build_dir" -G Ninja \
|
|
||||||
-DCMAKE_BUILD_TYPE="$config" \
|
|
||||||
-DCMAKE_PREFIX_PATH="$cmake_prefix_path" \
|
|
||||||
-DCMAKE_FRAMEWORK_PATH="${brew_prefix}/Frameworks;${brew_prefix}/opt/sparkle-framework/Frameworks" \
|
|
||||||
-DOPTIONAL_COMPONENTS_MISSING_DEPS_ARE_FATAL=OFF \
|
|
||||||
${cmake_extra_args+"${cmake_extra_args[@]}"} \
|
|
||||||
${icu_prefix:+-DICU_ROOT="$icu_prefix"}
|
|
||||||
|
|
||||||
run_with_heartbeat "Building" \
|
|
||||||
cmake --build "$build_dir" --parallel
|
|
||||||
|
|
||||||
if [[ "$do_deploy" -eq 1 ]]; then
|
|
||||||
echo "==> [$(ts)] Preparing env for 'deploy' target (GIO/GStreamer)"
|
|
||||||
export GIO_EXTRA_MODULES="${brew_prefix}/lib/gio/modules"
|
|
||||||
export GST_PLUGIN_SCANNER="${brew_prefix}/opt/gstreamer/libexec/gstreamer-1.0/gst-plugin-scanner"
|
|
||||||
export GST_PLUGIN_PATH="${brew_prefix}/lib/gstreamer-1.0"
|
|
||||||
|
|
||||||
# Optional, but helps dist/macos/macgstcopy.sh bundle libsoup which GStreamer loads dynamically.
|
|
||||||
libsoup_prefix="$(brew --prefix libsoup 2>/dev/null || true)"
|
|
||||||
if [[ -n "${libsoup_prefix}" ]]; then
|
|
||||||
libsoup_dylib="$(ls -1 "${libsoup_prefix}"/lib/libsoup-*.dylib 2>/dev/null | head -n 1 || true)"
|
|
||||||
if [[ -n "${libsoup_dylib}" ]]; then
|
|
||||||
export LIBSOUP_LIBRARY_PATH="${libsoup_dylib}"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
run_with_heartbeat "Running: deploy" \
|
|
||||||
cmake --build "$build_dir" --target deploy
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$do_dmg" -eq 1 ]]; then
|
|
||||||
run_with_heartbeat "Running: dmg" \
|
|
||||||
cmake --build "$build_dir" --target dmg
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Done"
|
|
||||||
echo "Built app:"
|
|
||||||
echo " ${build_dir}/strawberry.app"
|
|
||||||
|
|
||||||
@@ -1,324 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Guard: this script must be executed with bash (not sourced into zsh, not run via sh).
|
|
||||||
if [[ -z "${BASH_VERSION:-}" ]]; then
|
|
||||||
echo "Error: this script must be run with bash (it uses bash arrays)." >&2
|
|
||||||
echo "Run:" >&2
|
|
||||||
echo " bash ./build_tools/macos/build_mas_pkg.sh --run ..." >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
ts() { date +"%H:%M:%S"; }
|
|
||||||
lower() { echo "$1" | tr '[:upper:]' '[:lower:]'; }
|
|
||||||
|
|
||||||
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
repo_root="$(cd -- "${script_dir}/../.." && pwd)"
|
|
||||||
|
|
||||||
prepare_login_keychain_for_signing() {
|
|
||||||
# Some setups require explicitly granting Apple tooling access to the private key(s)
|
|
||||||
# (otherwise productbuild/codesign can fail with authorization errors like:
|
|
||||||
# CSSM Exception: -60008 Unable to obtain authorization for this operation
|
|
||||||
# or "User interaction is not allowed").
|
|
||||||
#
|
|
||||||
# This function is optional and only runs if a keychain password is provided.
|
|
||||||
local keychain_path="$HOME/Library/Keychains/login.keychain-db"
|
|
||||||
local pw="${1:-}"
|
|
||||||
|
|
||||||
if [[ -z "$pw" ]]; then
|
|
||||||
echo "==> [$(ts)] Note: no keychain password provided; skipping keychain access-control preparation."
|
|
||||||
echo " If productbuild later fails with -60008 authorization errors, fix it with either:"
|
|
||||||
echo " - Keychain Access → login → My Certificates → select the *private key* under the Installer cert → Get Info → Access Control → allow productbuild"
|
|
||||||
echo " - OR (CLI): security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k <password> \"$keychain_path\""
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Preparing login keychain for signing (unlock + key partition list)"
|
|
||||||
# Unlock so Security/Authorization can use keys without prompting.
|
|
||||||
security unlock-keychain -p "$pw" "$keychain_path" >/dev/null 2>&1 || true
|
|
||||||
# Allow Apple tools (codesign/productbuild) to access the private key without GUI prompts.
|
|
||||||
# This is the standard fix used for non-interactive signing.
|
|
||||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$pw" "$keychain_path" >/dev/null 2>&1 || true
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_keychain_search_list() {
|
|
||||||
# codesign builds the cert chain using the user keychain search list.
|
|
||||||
# If the list is missing the System keychain, you can get:
|
|
||||||
# "unable to build chain to self-signed root" + errSecInternalComponent
|
|
||||||
local login_kc="$HOME/Library/Keychains/login.keychain-db"
|
|
||||||
local system_kc="/Library/Keychains/System.keychain"
|
|
||||||
local roots_kc="/System/Library/Keychains/SystemRootCertificates.keychain"
|
|
||||||
|
|
||||||
local current
|
|
||||||
current="$(security list-keychains -d user 2>/dev/null | tr -d '"' | tr -d ' ' || true)"
|
|
||||||
|
|
||||||
if echo "$current" | grep -Fq "$system_kc"; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Note: adding System keychains to the user keychain search list (fixes common codesign chain errors)"
|
|
||||||
echo " (This changes the user keychain search list; run 'security list-keychains -d user' to view.)"
|
|
||||||
security list-keychains -d user -s "$login_kc" "$system_kc" "$roots_kc" >/dev/null 2>&1 || true
|
|
||||||
}
|
|
||||||
|
|
||||||
diagnose_chain_failure() {
|
|
||||||
echo "==> [$(ts)] Codesign failed. Common causes on macOS:"
|
|
||||||
echo " - System keychains not in the user keychain search list"
|
|
||||||
echo " - Missing/invalid WWDR intermediate certificate"
|
|
||||||
echo " - Keychain/key access issues"
|
|
||||||
echo
|
|
||||||
echo "==> [$(ts)] Keychain search list:"
|
|
||||||
security list-keychains -d user || true
|
|
||||||
echo
|
|
||||||
echo "==> [$(ts)] Checking for Apple WWDR intermediate in System keychain:"
|
|
||||||
security find-certificate -a -c "Apple Worldwide Developer Relations" /Library/Keychains/System.keychain 2>/dev/null | head -n 5 || true
|
|
||||||
echo
|
|
||||||
echo "==> [$(ts)] If WWDR is missing, install the current Apple WWDR intermediate certificate (via Xcode or Apple Developer portal)."
|
|
||||||
echo "==> [$(ts)] Then re-run this script."
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
preflight_identity() {
|
|
||||||
local what="$1"
|
|
||||||
local policy="$2"
|
|
||||||
local identity="$3"
|
|
||||||
|
|
||||||
# NOTE: security expects "-p <policy>" as *two* args; do not pass "-p codesigning" as one string.
|
|
||||||
if ! security find-identity -p "$policy" -v 2>/dev/null | grep -Fq "$identity"; then
|
|
||||||
echo "Error: ${what} identity not found/usable in Keychain: $identity" >&2
|
|
||||||
echo "Run: ./build_tools/macos/check_signing_identities.sh" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'EOF'
|
|
||||||
Usage:
|
|
||||||
./build_tools/macos/build_mas_pkg.sh --run [options]
|
|
||||||
|
|
||||||
What it does:
|
|
||||||
- Builds Strawberry in Mac App Store mode (BUILD_FOR_MAC_APP_STORE=ON)
|
|
||||||
- Runs deploy (macdeployqt + bundling) so the app bundle is self-contained
|
|
||||||
- Embeds a Mac App Store provisioning profile into the app bundle
|
|
||||||
- Codesigns the app with an Apple Distribution identity + entitlements
|
|
||||||
- Builds a signed .pkg suitable for uploading to App Store Connect
|
|
||||||
|
|
||||||
Required options:
|
|
||||||
--run
|
|
||||||
--codesign-identity "<name>" (e.g. "Apple Distribution: Dry Ark LLC (TEAMID)")
|
|
||||||
--installer-identity "<name>" (e.g. "3rd Party Mac Developer Installer: Dry Ark LLC (TEAMID)")
|
|
||||||
--provisionprofile <path> Path to a *Mac App Store* provisioning profile (*.provisionprofile)
|
|
||||||
|
|
||||||
Optional:
|
|
||||||
--release | --debug Build config (default: Release)
|
|
||||||
--clean Clean build dir before build
|
|
||||||
--build-dir <path> Override build directory
|
|
||||||
--entitlements <plist> Codesign entitlements (default: dist/macos/entitlements.mas.plist)
|
|
||||||
--bundle-id <id> Override CFBundleIdentifier (default: com.dryark.strawberry)
|
|
||||||
--pkg-out <path> Output .pkg path (default: <build-dir>/strawberry-mas.pkg)
|
|
||||||
--keychain-password <pw> OPTIONAL: unlock/login keychain + set key partition list for Apple tools
|
|
||||||
(alternative: set env var STRAWBERRY_KEYCHAIN_PASSWORD)
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
# Tip: if your keychain password contains characters like ! or $, prefer the env var or single quotes.
|
|
||||||
STRAWBERRY_KEYCHAIN_PASSWORD='your-login-keychain-password' \
|
|
||||||
./build_tools/macos/build_mas_pkg.sh --run --release --clean \
|
|
||||||
--codesign-identity "Apple Distribution: Your Name (TEAMID)" \
|
|
||||||
--installer-identity "3rd Party Mac Developer Installer: Your Name (TEAMID)" \
|
|
||||||
--provisionprofile "$HOME/Library/MobileDevice/Provisioning Profiles/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX.provisionprofile"
|
|
||||||
|
|
||||||
./build_tools/macos/build_mas_pkg.sh --run --release --clean \
|
|
||||||
--codesign-identity "Apple Distribution: Your Name (TEAMID)" \
|
|
||||||
--installer-identity "3rd Party Mac Developer Installer: Your Name (TEAMID)" \
|
|
||||||
--provisionprofile "$HOME/Library/MobileDevice/Provisioning Profiles/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX.provisionprofile"
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
- Mac App Store submissions do NOT use Developer ID notarization.
|
|
||||||
- You must create a Mac App Store provisioning profile for your App ID in Apple Developer.
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
if [[ "$(uname -s)" != "Darwin" ]]; then
|
|
||||||
echo "Error: This script is for macOS only." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
do_run=0
|
|
||||||
config="Release"
|
|
||||||
do_clean=0
|
|
||||||
build_dir=""
|
|
||||||
codesign_identity=""
|
|
||||||
installer_identity=""
|
|
||||||
provisionprofile=""
|
|
||||||
entitlements=""
|
|
||||||
bundle_id="com.dryark.strawberry"
|
|
||||||
pkg_out=""
|
|
||||||
keychain_password="${STRAWBERRY_KEYCHAIN_PASSWORD:-}"
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--run) do_run=1; shift ;;
|
|
||||||
--release) config="Release"; shift ;;
|
|
||||||
--debug) config="Debug"; shift ;;
|
|
||||||
--clean) do_clean=1; shift ;;
|
|
||||||
--build-dir) build_dir="${2:-}"; shift 2 ;;
|
|
||||||
--codesign-identity) codesign_identity="${2:-}"; shift 2 ;;
|
|
||||||
--installer-identity) installer_identity="${2:-}"; shift 2 ;;
|
|
||||||
--provisionprofile) provisionprofile="${2:-}"; shift 2 ;;
|
|
||||||
--entitlements) entitlements="${2:-}"; shift 2 ;;
|
|
||||||
--bundle-id) bundle_id="${2:-}"; shift 2 ;;
|
|
||||||
--pkg-out) pkg_out="${2:-}"; shift 2 ;;
|
|
||||||
--keychain-password) keychain_password="${2:-}"; shift 2 ;;
|
|
||||||
-h|--help) usage; exit 0 ;;
|
|
||||||
*) echo "Unknown arg: $1" >&2; usage; exit 2 ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ "$do_run" -eq 0 ]]; then
|
|
||||||
usage
|
|
||||||
echo
|
|
||||||
echo "==> [$(ts)] Tip: list available signing identities:"
|
|
||||||
echo " security find-identity -p codesigning -v"
|
|
||||||
echo " security find-identity -p basic -v"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -z "$codesign_identity" ]]; then
|
|
||||||
echo "Error: missing --codesign-identity" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
if [[ -z "$installer_identity" ]]; then
|
|
||||||
echo "Error: missing --installer-identity" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
if [[ -z "$provisionprofile" || ! -f "$provisionprofile" ]]; then
|
|
||||||
echo "Error: missing/invalid --provisionprofile: $provisionprofile" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -z "$entitlements" ]]; then
|
|
||||||
entitlements="${repo_root}/dist/macos/entitlements.mas.plist"
|
|
||||||
fi
|
|
||||||
if [[ ! -f "$entitlements" ]]; then
|
|
||||||
echo "Error: entitlements file not found: $entitlements" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -z "$build_dir" ]]; then
|
|
||||||
build_dir="${repo_root}/cmake-build-macos-$(lower "$config")-mas"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -z "$pkg_out" ]]; then
|
|
||||||
pkg_out="${build_dir}/strawberry-mas.pkg"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Repo: ${repo_root}"
|
|
||||||
echo "==> [$(ts)] Build dir: ${build_dir}"
|
|
||||||
echo "==> [$(ts)] Config: ${config}"
|
|
||||||
echo "==> [$(ts)] Bundle ID: ${bundle_id}"
|
|
||||||
echo "==> [$(ts)] Entitlements: ${entitlements}"
|
|
||||||
echo "==> [$(ts)] Provisioning profile: ${provisionprofile}"
|
|
||||||
echo "==> [$(ts)] Output pkg: ${pkg_out}"
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Building (Mac App Store mode)"
|
|
||||||
build_args=( "--release" )
|
|
||||||
if [[ "$config" == "Debug" ]]; then build_args=( "--debug" ); fi
|
|
||||||
if [[ "$do_clean" -eq 1 ]]; then build_args+=( "--clean" ); fi
|
|
||||||
build_args+=( "--build-dir" "$build_dir" "--mas" "--deploy" )
|
|
||||||
|
|
||||||
# Provide bundle id via CMake cache variable.
|
|
||||||
export MACOS_BUNDLE_ID="$bundle_id"
|
|
||||||
|
|
||||||
"${repo_root}/build_tools/macos/build_app.sh" "${build_args[@]}"
|
|
||||||
|
|
||||||
app_path="${build_dir}/strawberry.app"
|
|
||||||
bin_path="${app_path}/Contents/MacOS/strawberry"
|
|
||||||
if [[ ! -x "$bin_path" ]]; then
|
|
||||||
echo "Error: built app not found at: $app_path" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Embedding provisioning profile"
|
|
||||||
cp -f "$provisionprofile" "${app_path}/Contents/embedded.provisionprofile"
|
|
||||||
|
|
||||||
ensure_keychain_search_list
|
|
||||||
prepare_login_keychain_for_signing "$keychain_password"
|
|
||||||
preflight_identity "codesign" "codesigning" "$codesign_identity"
|
|
||||||
preflight_identity "installer" "basic" "$installer_identity"
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Codesigning app (Mac App Store)"
|
|
||||||
codesign_args=( --force --timestamp --options runtime --sign "$codesign_identity" --entitlements "$entitlements" )
|
|
||||||
|
|
||||||
# Clean up any leftover codesign temp files from previous interrupted runs.
|
|
||||||
find "$app_path" -name "*.cstemp" -print0 2>/dev/null | while IFS= read -r -d '' f; do
|
|
||||||
rm -f "$f" || true
|
|
||||||
done
|
|
||||||
|
|
||||||
# Clear macOS provenance/quarantine metadata which can interfere with modifying files in-place.
|
|
||||||
xattr -dr com.apple.provenance "$app_path" >/dev/null 2>&1 || true
|
|
||||||
xattr -dr com.apple.quarantine "$app_path" >/dev/null 2>&1 || true
|
|
||||||
|
|
||||||
# Sign nested code first, then frameworks, then the main app bundle.
|
|
||||||
find "$app_path" -type f \( -name "*.dylib" -o -name "*.so" -o -perm -111 \) \
|
|
||||||
! -name "*.cstemp" \
|
|
||||||
! -path "*/Contents/Frameworks/*.framework/*" \
|
|
||||||
! -path "*/Contents/Frameworks/*.app/*" \
|
|
||||||
! -path "*/Contents/Frameworks/*.xpc/*" \
|
|
||||||
! -path "*/Contents/PlugIns/*.framework/*" \
|
|
||||||
! -path "*/Contents/PlugIns/*.app/*" \
|
|
||||||
! -path "*/Contents/PlugIns/*.xpc/*" \
|
|
||||||
-print0 | while IFS= read -r -d '' f; do
|
|
||||||
# Only sign Mach-O binaries.
|
|
||||||
if file -b "$f" | grep -q "Mach-O"; then
|
|
||||||
codesign "${codesign_args[@]}" "$f" >/dev/null
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
find "$app_path" -type d \( -name "*.xpc" -o -name "*.app" \) -print0 2>/dev/null | while IFS= read -r -d '' b; do
|
|
||||||
codesign "${codesign_args[@]}" "$b" >/dev/null
|
|
||||||
done
|
|
||||||
|
|
||||||
find "$app_path/Contents/Frameworks" "$app_path/Contents/PlugIns" -type d -name "*.framework" -print0 2>/dev/null | while IFS= read -r -d '' fw; do
|
|
||||||
codesign "${codesign_args[@]}" "$fw" >/dev/null
|
|
||||||
done
|
|
||||||
|
|
||||||
codesign "${codesign_args[@]}" "$app_path" >/dev/null
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Verifying codesign"
|
|
||||||
codesign --verify --deep --strict --verbose=2 "$app_path"
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Building signed .pkg for App Store upload"
|
|
||||||
rm -f "$pkg_out" >/dev/null 2>&1 || true
|
|
||||||
|
|
||||||
if ! productbuild \
|
|
||||||
--component "$app_path" /Applications \
|
|
||||||
--sign "$installer_identity" \
|
|
||||||
"$pkg_out"; then
|
|
||||||
echo "Error: productbuild failed while signing the .pkg." >&2
|
|
||||||
echo "Common cause: keychain/private-key authorization (e.g. CSSM -60008)." >&2
|
|
||||||
echo >&2
|
|
||||||
echo "Fix options:" >&2
|
|
||||||
echo "1) Keychain Access UI:" >&2
|
|
||||||
echo " - Keychain Access → login → My Certificates" >&2
|
|
||||||
echo " - Find: $installer_identity" >&2
|
|
||||||
echo " - Expand it and select the *private key* under it" >&2
|
|
||||||
echo " - Get Info → Access Control → allow /usr/bin/productbuild (optionally also allow /usr/bin/pkgbuild)" >&2
|
|
||||||
echo "2) CLI (recommended for repeatable builds):" >&2
|
|
||||||
echo " security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k <password> \"$HOME/Library/Keychains/login.keychain-db\"" >&2
|
|
||||||
echo >&2
|
|
||||||
echo "Tip: you can also rerun this script with:" >&2
|
|
||||||
echo " --keychain-password <login-keychain-password>" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Verifying pkg signature"
|
|
||||||
pkgutil --check-signature "$pkg_out" || true
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "Done."
|
|
||||||
echo "App: $app_path"
|
|
||||||
echo "PKG: $pkg_out"
|
|
||||||
|
|
||||||
@@ -1,241 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Build a universal (arm64+x86_64) Mac App Store upload package by:
|
|
||||||
# - merging two already-deployed Strawberry.app bundles (arm64 + x86_64) using lipo
|
|
||||||
# - embedding a Mac App Store provisioning profile
|
|
||||||
# - codesigning with Apple Distribution (+ entitlements)
|
|
||||||
# - producing a signed .pkg with productbuild (Installer identity)
|
|
||||||
#
|
|
||||||
# Intended workflow with two Macs:
|
|
||||||
# 1) On Apple Silicon Mac: build+deploy MAS app bundle (unsigned) → copy strawberry.app somewhere
|
|
||||||
# 2) On Intel Mac: build+deploy MAS app bundle (unsigned) → copy strawberry.app somewhere
|
|
||||||
# 3) On the Mac that holds your signing keys (either one): run THIS script to merge+sign+package
|
|
||||||
|
|
||||||
ts() { date +"%H:%M:%S"; }
|
|
||||||
|
|
||||||
if [[ -z "${BASH_VERSION:-}" ]]; then
|
|
||||||
echo "Error: this script must be run with bash." >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
if [[ "$(uname -s)" != "Darwin" ]]; then
|
|
||||||
echo "Error: This script is for macOS only." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
repo_root="$(cd -- "${script_dir}/../.." && pwd)"
|
|
||||||
|
|
||||||
ensure_keychain_search_list() {
|
|
||||||
local login_kc="$HOME/Library/Keychains/login.keychain-db"
|
|
||||||
local system_kc="/Library/Keychains/System.keychain"
|
|
||||||
local roots_kc="/System/Library/Keychains/SystemRootCertificates.keychain"
|
|
||||||
|
|
||||||
local current
|
|
||||||
current="$(security list-keychains -d user 2>/dev/null | tr -d '"' | tr -d ' ' || true)"
|
|
||||||
if echo "$current" | grep -Fq "$system_kc"; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
echo "==> [$(ts)] Note: adding System keychains to the user keychain search list"
|
|
||||||
security list-keychains -d user -s "$login_kc" "$system_kc" "$roots_kc" >/dev/null 2>&1 || true
|
|
||||||
}
|
|
||||||
|
|
||||||
prepare_login_keychain_for_signing() {
|
|
||||||
local keychain_path="$HOME/Library/Keychains/login.keychain-db"
|
|
||||||
local pw="${1:-}"
|
|
||||||
if [[ -z "$pw" ]]; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
echo "==> [$(ts)] Preparing login keychain for signing (unlock + key partition list)"
|
|
||||||
security unlock-keychain -p "$pw" "$keychain_path" >/dev/null 2>&1 || true
|
|
||||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$pw" "$keychain_path" >/dev/null 2>&1 || true
|
|
||||||
}
|
|
||||||
|
|
||||||
preflight_identity() {
|
|
||||||
local what="$1"
|
|
||||||
local policy="$2"
|
|
||||||
local identity="$3"
|
|
||||||
if ! security find-identity -p "$policy" -v 2>/dev/null | grep -Fq "$identity"; then
|
|
||||||
echo "Error: ${what} identity not found/usable in Keychain: $identity" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'EOF'
|
|
||||||
Usage:
|
|
||||||
./build_tools/macos/build_mas_universal_pkg.sh --run [options]
|
|
||||||
|
|
||||||
Required:
|
|
||||||
--run
|
|
||||||
--arm-app <path> Path to arm64 Strawberry.app (already built+deployed, unsigned)
|
|
||||||
--x86-app <path> Path to x86_64 Strawberry.app (already built+deployed, unsigned)
|
|
||||||
--codesign-identity "<name>" Apple Distribution: ...
|
|
||||||
--installer-identity "<name>" 3rd Party Mac Developer Installer: ...
|
|
||||||
--provisionprofile <path> Mac App Store provisioning profile (*.provisionprofile)
|
|
||||||
|
|
||||||
Optional:
|
|
||||||
--out-dir <path> Output directory (default: cmake-build-macos-release-mas-universal)
|
|
||||||
--entitlements <plist> Codesign entitlements (default: dist/macos/entitlements.mas.plist)
|
|
||||||
--pkg-out <path> Output .pkg path (default: <out-dir>/strawberry-mas-universal.pkg)
|
|
||||||
--bundle-id <id> For display/logging only (does not rewrite Info.plist)
|
|
||||||
--keychain-password <pw> Or set env var STRAWBERRY_KEYCHAIN_PASSWORD (quote it!)
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
- This script does NOT build Strawberry. It merges two pre-built app bundles.
|
|
||||||
- After lipo-merging, the app must be re-signed (this script does that).
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
do_run=0
|
|
||||||
arm_app=""
|
|
||||||
x86_app=""
|
|
||||||
out_dir=""
|
|
||||||
codesign_identity=""
|
|
||||||
installer_identity=""
|
|
||||||
provisionprofile=""
|
|
||||||
entitlements=""
|
|
||||||
pkg_out=""
|
|
||||||
bundle_id=""
|
|
||||||
keychain_password="${STRAWBERRY_KEYCHAIN_PASSWORD:-}"
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--run) do_run=1; shift ;;
|
|
||||||
--arm-app) arm_app="${2:-}"; shift 2 ;;
|
|
||||||
--x86-app) x86_app="${2:-}"; shift 2 ;;
|
|
||||||
--out-dir) out_dir="${2:-}"; shift 2 ;;
|
|
||||||
--codesign-identity) codesign_identity="${2:-}"; shift 2 ;;
|
|
||||||
--installer-identity) installer_identity="${2:-}"; shift 2 ;;
|
|
||||||
--provisionprofile) provisionprofile="${2:-}"; shift 2 ;;
|
|
||||||
--entitlements) entitlements="${2:-}"; shift 2 ;;
|
|
||||||
--pkg-out) pkg_out="${2:-}"; shift 2 ;;
|
|
||||||
--bundle-id) bundle_id="${2:-}"; shift 2 ;;
|
|
||||||
--keychain-password) keychain_password="${2:-}"; shift 2 ;;
|
|
||||||
-h|--help) usage; exit 0 ;;
|
|
||||||
*) echo "Unknown arg: $1" >&2; usage; exit 2 ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ "$do_run" -eq 0 ]]; then
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -z "$arm_app" || ! -d "$arm_app" ]]; then
|
|
||||||
echo "Error: missing/invalid --arm-app: $arm_app" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
if [[ -z "$x86_app" || ! -d "$x86_app" ]]; then
|
|
||||||
echo "Error: missing/invalid --x86-app: $x86_app" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
if [[ -z "$codesign_identity" ]]; then
|
|
||||||
echo "Error: missing --codesign-identity" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
if [[ -z "$installer_identity" ]]; then
|
|
||||||
echo "Error: missing --installer-identity" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
if [[ -z "$provisionprofile" || ! -f "$provisionprofile" ]]; then
|
|
||||||
echo "Error: missing/invalid --provisionprofile: $provisionprofile" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -z "$entitlements" ]]; then
|
|
||||||
entitlements="${repo_root}/dist/macos/entitlements.mas.plist"
|
|
||||||
fi
|
|
||||||
if [[ ! -f "$entitlements" ]]; then
|
|
||||||
echo "Error: entitlements file not found: $entitlements" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -z "$out_dir" ]]; then
|
|
||||||
out_dir="${repo_root}/cmake-build-macos-release-mas-universal"
|
|
||||||
fi
|
|
||||||
mkdir -p "$out_dir"
|
|
||||||
|
|
||||||
universal_app="${out_dir}/strawberry.app"
|
|
||||||
if [[ -e "$universal_app" ]]; then
|
|
||||||
rm -rf "$universal_app"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Repo: $repo_root"
|
|
||||||
echo "==> [$(ts)] arm app: $arm_app"
|
|
||||||
echo "==> [$(ts)] x86 app: $x86_app"
|
|
||||||
echo "==> [$(ts)] out dir: $out_dir"
|
|
||||||
if [[ -n "$bundle_id" ]]; then
|
|
||||||
echo "==> [$(ts)] bundle id (expected): $bundle_id"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Creating universal app bundle (lipo merge)"
|
|
||||||
"${repo_root}/build_tools/macos/make_universal_app.sh" \
|
|
||||||
--arm-app "$arm_app" \
|
|
||||||
--x86-app "$x86_app" \
|
|
||||||
--out-app "$universal_app" \
|
|
||||||
--clean
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Embedding provisioning profile"
|
|
||||||
cp -f "$provisionprofile" "${universal_app}/Contents/embedded.provisionprofile"
|
|
||||||
|
|
||||||
ensure_keychain_search_list
|
|
||||||
prepare_login_keychain_for_signing "$keychain_password"
|
|
||||||
preflight_identity "codesign" "codesigning" "$codesign_identity"
|
|
||||||
preflight_identity "installer" "basic" "$installer_identity"
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Codesigning universal app (Mac App Store)"
|
|
||||||
codesign_args=( --force --timestamp --options runtime --sign "$codesign_identity" --entitlements "$entitlements" )
|
|
||||||
|
|
||||||
# Clean up any leftover codesign temp files and xattrs.
|
|
||||||
find "$universal_app" -name "*.cstemp" -print0 2>/dev/null | while IFS= read -r -d '' f; do rm -f "$f" || true; done
|
|
||||||
xattr -dr com.apple.provenance "$universal_app" >/dev/null 2>&1 || true
|
|
||||||
xattr -dr com.apple.quarantine "$universal_app" >/dev/null 2>&1 || true
|
|
||||||
|
|
||||||
# Sign nested code first, then frameworks, then the main app bundle.
|
|
||||||
find "$universal_app" -type f \( -name "*.dylib" -o -name "*.so" -o -perm -111 \) \
|
|
||||||
! -name "*.cstemp" \
|
|
||||||
! -path "*/Contents/Frameworks/*.framework/*" \
|
|
||||||
! -path "*/Contents/Frameworks/*.app/*" \
|
|
||||||
! -path "*/Contents/Frameworks/*.xpc/*" \
|
|
||||||
! -path "*/Contents/PlugIns/*.framework/*" \
|
|
||||||
! -path "*/Contents/PlugIns/*.app/*" \
|
|
||||||
! -path "*/Contents/PlugIns/*.xpc/*" \
|
|
||||||
-print0 | while IFS= read -r -d '' f; do
|
|
||||||
if /usr/bin/file -b "$f" | grep -q "Mach-O"; then
|
|
||||||
codesign "${codesign_args[@]}" "$f" >/dev/null
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
find "$universal_app" -type d \( -name "*.xpc" -o -name "*.app" \) -print0 2>/dev/null | while IFS= read -r -d '' b; do
|
|
||||||
codesign "${codesign_args[@]}" "$b" >/dev/null
|
|
||||||
done
|
|
||||||
|
|
||||||
find "$universal_app/Contents/Frameworks" "$universal_app/Contents/PlugIns" -type d -name "*.framework" -print0 2>/dev/null | while IFS= read -r -d '' fw; do
|
|
||||||
codesign "${codesign_args[@]}" "$fw" >/dev/null
|
|
||||||
done
|
|
||||||
|
|
||||||
codesign "${codesign_args[@]}" "$universal_app" >/dev/null
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Verifying codesign"
|
|
||||||
codesign --verify --deep --strict --verbose=2 "$universal_app"
|
|
||||||
|
|
||||||
if [[ -z "$pkg_out" ]]; then
|
|
||||||
pkg_out="${out_dir}/strawberry-mas-universal.pkg"
|
|
||||||
fi
|
|
||||||
rm -f "$pkg_out" >/dev/null 2>&1 || true
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Building signed .pkg for App Store upload"
|
|
||||||
productbuild \
|
|
||||||
--component "$universal_app" /Applications \
|
|
||||||
--sign "$installer_identity" \
|
|
||||||
"$pkg_out"
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Verifying pkg signature"
|
|
||||||
pkgutil --check-signature "$pkg_out" || true
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "Done."
|
|
||||||
echo "Universal app: $universal_app"
|
|
||||||
echo "PKG: $pkg_out"
|
|
||||||
|
|
||||||
@@ -1,309 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
ts() { date +"%H:%M:%S"; }
|
|
||||||
|
|
||||||
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
repo_root="$(cd -- "${script_dir}/../.." && pwd)"
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'EOF'
|
|
||||||
Usage:
|
|
||||||
./build_tools/macos/build_sign_notarize.sh # list local signing identities + notary profiles
|
|
||||||
./build_tools/macos/build_sign_notarize.sh --run [options] # build, sign, notarize, staple
|
|
||||||
|
|
||||||
Common options:
|
|
||||||
--run Perform build/sign/notarize (otherwise list identities/profiles)
|
|
||||||
--release | --debug Build config (default: Release)
|
|
||||||
--clean Clean build dir before build
|
|
||||||
--deploy Run CMake 'deploy' target before signing (default: on)
|
|
||||||
--no-deploy Do not run 'deploy' (not recommended for distribution)
|
|
||||||
--dmg Build a DMG after app notarization, then notarize+staple the DMG too
|
|
||||||
--build-dir <path> Override build directory
|
|
||||||
|
|
||||||
Signing options:
|
|
||||||
--identity "<name>" Codesign identity (e.g. "Developer ID Application: Your Name (TEAMID)")
|
|
||||||
--entitlements <plist> Optional entitlements plist for codesign
|
|
||||||
|
|
||||||
Notarization options (recommended):
|
|
||||||
--notary-profile <name> notarytool keychain profile name (created via `xcrun notarytool store-credentials <name> ...`)
|
|
||||||
--skip-notarize Skip notarization
|
|
||||||
|
|
||||||
Outputs:
|
|
||||||
- Signed app: <build-dir>/strawberry.app
|
|
||||||
- Zip for notarization: <build-dir>/strawberry-notarize.zip
|
|
||||||
- DMG (optional): <build-dir>/strawberry-*.dmg
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
- This script is intended for Developer ID distribution (outside Mac App Store).
|
|
||||||
- If you want Sparkle updates, you'll typically ship a notarized .zip + an appcast feed.
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
list_identities_and_profiles() {
|
|
||||||
echo "==> [$(ts)] macOS code signing identities (Keychain)"
|
|
||||||
security find-identity -p codesigning -v || true
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "==> [$(ts)] notarytool credential profiles"
|
|
||||||
echo "Note: this Xcode notarytool version does not provide a 'list-profiles' command."
|
|
||||||
echo "If you forgot the profile name you created, check Keychain Access or re-run:"
|
|
||||||
echo " xcrun notarytool store-credentials \"<profile-name>\" --apple-id \"you@example.com\" --team-id \"TEAMID\""
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "==> [$(ts)] Provisioning profiles (macOS)"
|
|
||||||
prof_dir="${HOME}/Library/MobileDevice/Provisioning Profiles"
|
|
||||||
if [[ -d "${prof_dir}" ]]; then
|
|
||||||
ls -la "${prof_dir}" | head -n 50
|
|
||||||
else
|
|
||||||
echo "(none found at '${prof_dir}')"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
if [[ "$(uname -s)" != "Darwin" ]]; then
|
|
||||||
echo "Error: This script is for macOS only." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! command -v xcode-select >/dev/null 2>&1 || ! xcode-select -p >/dev/null 2>&1; then
|
|
||||||
echo "Error: Xcode Command Line Tools not found." >&2
|
|
||||||
echo "Install them first: xcode-select --install" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
do_run=0
|
|
||||||
config="Release"
|
|
||||||
do_clean=0
|
|
||||||
do_deploy=1
|
|
||||||
do_dmg=0
|
|
||||||
build_dir=""
|
|
||||||
identity=""
|
|
||||||
entitlements=""
|
|
||||||
notary_profile=""
|
|
||||||
skip_notarize=0
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--run) do_run=1; shift ;;
|
|
||||||
--release) config="Release"; shift ;;
|
|
||||||
--debug) config="Debug"; shift ;;
|
|
||||||
--clean) do_clean=1; shift ;;
|
|
||||||
--deploy) do_deploy=1; shift ;;
|
|
||||||
--no-deploy) do_deploy=0; shift ;;
|
|
||||||
--dmg) do_dmg=1; shift ;;
|
|
||||||
--build-dir) build_dir="${2:-}"; shift 2 ;;
|
|
||||||
--identity) identity="${2:-}"; shift 2 ;;
|
|
||||||
--entitlements) entitlements="${2:-}"; shift 2 ;;
|
|
||||||
--notary-profile) notary_profile="${2:-}"; shift 2 ;;
|
|
||||||
--skip-notarize) skip_notarize=1; shift ;;
|
|
||||||
-h|--help) usage; exit 0 ;;
|
|
||||||
*) echo "Unknown arg: $1" >&2; usage; exit 2 ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ "$do_run" -eq 0 ]]; then
|
|
||||||
usage
|
|
||||||
echo
|
|
||||||
list_identities_and_profiles
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -z "$build_dir" ]]; then
|
|
||||||
lc_config="$(echo "$config" | tr '[:upper:]' '[:lower:]')"
|
|
||||||
build_dir="${repo_root}/cmake-build-macos-${lc_config}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
app_path="${build_dir}/strawberry.app"
|
|
||||||
bin_path="${app_path}/Contents/MacOS/strawberry"
|
|
||||||
zip_path="${build_dir}/strawberry-notarize.zip"
|
|
||||||
dmg_path=""
|
|
||||||
|
|
||||||
notarize_and_maybe_staple() {
|
|
||||||
local file_path="$1"
|
|
||||||
local label="$2"
|
|
||||||
local do_staple="${3:-1}"
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Notarizing ${label}"
|
|
||||||
local out
|
|
||||||
out="$(mktemp -t notarytool-submit.XXXXXX)"
|
|
||||||
xcrun notarytool submit "$file_path" --keychain-profile "$notary_profile" --wait --output-format plist --no-progress >"$out"
|
|
||||||
|
|
||||||
local submit_id submit_status
|
|
||||||
submit_id="$(/usr/bin/plutil -extract id raw -o - "$out" 2>/dev/null || true)"
|
|
||||||
submit_status="$(/usr/bin/plutil -extract status raw -o - "$out" 2>/dev/null || true)"
|
|
||||||
rm -f "$out" || true
|
|
||||||
|
|
||||||
if [[ -z "$submit_id" ]]; then
|
|
||||||
echo "Error: could not parse notarization submission id for ${label}." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Notary submission id: $submit_id"
|
|
||||||
echo "==> [$(ts)] Notary status: $submit_status"
|
|
||||||
|
|
||||||
if [[ "$submit_status" != "Accepted" ]]; then
|
|
||||||
echo "Error: notarization failed for ${label} with status '$submit_status'. Fetching log..." >&2
|
|
||||||
xcrun notarytool log "$submit_id" --keychain-profile "$notary_profile" || true
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$do_staple" -eq 1 ]]; then
|
|
||||||
echo "==> [$(ts)] Stapling ${label}"
|
|
||||||
xcrun stapler staple "$file_path"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
if [[ -z "$identity" ]]; then
|
|
||||||
echo "Error: Missing --identity (Developer ID Application identity)." >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$skip_notarize" -eq 0 && -z "$notary_profile" ]]; then
|
|
||||||
echo "Error: Missing --notary-profile (or pass --skip-notarize)." >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Building Strawberry"
|
|
||||||
build_args=( "--release" )
|
|
||||||
if [[ "$config" == "Debug" ]]; then build_args=( "--debug" ); fi
|
|
||||||
if [[ "$do_clean" -eq 1 ]]; then build_args+=( "--clean" ); fi
|
|
||||||
if [[ -n "$build_dir" ]]; then build_args+=( "--build-dir" "$build_dir" ); fi
|
|
||||||
if [[ "$do_deploy" -eq 1 ]]; then build_args+=( "--deploy" ); fi
|
|
||||||
|
|
||||||
"${repo_root}/build_tools/macos/build_app.sh" "${build_args[@]}"
|
|
||||||
|
|
||||||
if [[ ! -x "$bin_path" ]]; then
|
|
||||||
echo "Error: built app not found at: $app_path" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Codesigning (hardened runtime)"
|
|
||||||
codesign_args=( --force --timestamp --options runtime --sign "$identity" )
|
|
||||||
if [[ -n "$entitlements" ]]; then
|
|
||||||
codesign_args+=( --entitlements "$entitlements" )
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Clean up any leftover codesign temp files from previous interrupted runs.
|
|
||||||
# codesign may create *.cstemp alongside binaries while updating signatures.
|
|
||||||
find "$app_path" -name "*.cstemp" -print0 2>/dev/null | while IFS= read -r -d '' f; do
|
|
||||||
rm -f "$f" || true
|
|
||||||
done
|
|
||||||
|
|
||||||
# Clear macOS provenance/quarantine metadata which can interfere with modifying files in-place.
|
|
||||||
xattr -dr com.apple.provenance "$app_path" >/dev/null 2>&1 || true
|
|
||||||
xattr -dr com.apple.quarantine "$app_path" >/dev/null 2>&1 || true
|
|
||||||
|
|
||||||
# Sign nested code first, then frameworks, then the main app bundle.
|
|
||||||
#
|
|
||||||
# Important: do NOT codesign individual files *inside* a .framework bundle (e.g. Sparkle.framework/Sparkle),
|
|
||||||
# because codesign expects frameworks to be signed as bundles and may error with
|
|
||||||
# "bundle format is ambiguous (could be app or framework)".
|
|
||||||
|
|
||||||
# 1) Sign dylibs and standalone executables that are NOT inside a .framework/.app/.xpc bundle.
|
|
||||||
find "$app_path" -type f \( -name "*.dylib" -o -name "*.so" -o -perm -111 \) \
|
|
||||||
! -name "*.cstemp" \
|
|
||||||
! -path "*/Contents/Frameworks/*.framework/*" \
|
|
||||||
! -path "*/Contents/Frameworks/*.app/*" \
|
|
||||||
! -path "*/Contents/Frameworks/*.xpc/*" \
|
|
||||||
! -path "*/Contents/PlugIns/*.framework/*" \
|
|
||||||
! -path "*/Contents/PlugIns/*.app/*" \
|
|
||||||
! -path "*/Contents/PlugIns/*.xpc/*" \
|
|
||||||
-print0 | while IFS= read -r -d '' f; do
|
|
||||||
codesign "${codesign_args[@]}" "$f" >/dev/null
|
|
||||||
done
|
|
||||||
|
|
||||||
# 2) Sign nested helper apps and XPC services (Sparkle ships these inside its framework).
|
|
||||||
find "$app_path" -type d \( -name "*.xpc" -o -name "*.app" \) -print0 2>/dev/null | while IFS= read -r -d '' b; do
|
|
||||||
codesign "${codesign_args[@]}" "$b" >/dev/null
|
|
||||||
done
|
|
||||||
|
|
||||||
# 2b) Sparkle.framework contains a standalone helper executable "Autoupdate" under Versions/* that is
|
|
||||||
# not inside an .app or .xpc bundle. Notarization requires it be signed with Developer ID + timestamp.
|
|
||||||
sparkle_fw="$app_path/Contents/Frameworks/Sparkle.framework"
|
|
||||||
if [[ -d "$sparkle_fw" ]]; then
|
|
||||||
find "$sparkle_fw/Versions" -type f -perm -111 \
|
|
||||||
! -name "*.cstemp" \
|
|
||||||
! -path "*/_CodeSignature/*" \
|
|
||||||
-print0 2>/dev/null | while IFS= read -r -d '' f; do
|
|
||||||
codesign "${codesign_args[@]}" "$f" >/dev/null
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 3) Sign frameworks as bundles.
|
|
||||||
find "$app_path/Contents/Frameworks" "$app_path/Contents/PlugIns" -type d -name "*.framework" -print0 2>/dev/null | while IFS= read -r -d '' fw; do
|
|
||||||
codesign "${codesign_args[@]}" "$fw" >/dev/null
|
|
||||||
done
|
|
||||||
|
|
||||||
# 4) Finally sign the main app.
|
|
||||||
codesign "${codesign_args[@]}" "$app_path" >/dev/null
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Verifying codesign"
|
|
||||||
codesign --verify --deep --strict --verbose=2 "$app_path"
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Creating zip for notarization"
|
|
||||||
rm -f "$zip_path"
|
|
||||||
ditto -c -k --sequesterRsrc --keepParent "$app_path" "$zip_path"
|
|
||||||
|
|
||||||
if [[ "$skip_notarize" -eq 0 ]]; then
|
|
||||||
# ZIP archives cannot be stapled; notarization is still useful for distribution and Sparkle.
|
|
||||||
notarize_and_maybe_staple "$zip_path" "ZIP" 0
|
|
||||||
echo "==> [$(ts)] Stapling app"
|
|
||||||
xcrun stapler staple "$app_path"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$do_dmg" -eq 1 ]]; then
|
|
||||||
echo "==> [$(ts)] Building DMG (from already-signed app; no redeploy)"
|
|
||||||
if ! command -v create-dmg >/dev/null 2>&1; then
|
|
||||||
echo "Error: create-dmg not found. Install it with Homebrew (it's in Brewfile):" >&2
|
|
||||||
echo " ./build_tools/macos/install_brew_deps.sh" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Build a versioned DMG name using Info.plist (falls back to Strawberry version constant).
|
|
||||||
plist="${app_path}/Contents/Info.plist"
|
|
||||||
bundle_version="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleVersion' "$plist" 2>/dev/null || true)"
|
|
||||||
if [[ -z "${bundle_version}" ]]; then
|
|
||||||
bundle_version="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$plist" 2>/dev/null || true)"
|
|
||||||
fi
|
|
||||||
if [[ -z "${bundle_version}" ]]; then
|
|
||||||
bundle_version="unknown"
|
|
||||||
fi
|
|
||||||
arch="$(uname -m)"
|
|
||||||
dmg_path="${build_dir}/strawberry-${bundle_version}-${arch}.dmg"
|
|
||||||
|
|
||||||
rm -f "$dmg_path"
|
|
||||||
(
|
|
||||||
cd "$build_dir"
|
|
||||||
create-dmg \
|
|
||||||
--volname strawberry \
|
|
||||||
--background "${repo_root}/dist/macos/dmg_background.png" \
|
|
||||||
--app-drop-link 450 218 \
|
|
||||||
--icon strawberry.app 150 218 \
|
|
||||||
--window-size 600 450 \
|
|
||||||
"$(basename "$dmg_path")" \
|
|
||||||
strawberry.app
|
|
||||||
)
|
|
||||||
if [[ -z "$dmg_path" ]]; then
|
|
||||||
echo "Error: DMG was not created in $build_dir" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Codesigning DMG"
|
|
||||||
codesign --force --timestamp --sign "$identity" "$dmg_path"
|
|
||||||
|
|
||||||
if [[ "$skip_notarize" -eq 0 ]]; then
|
|
||||||
notarize_and_maybe_staple "$dmg_path" "DMG" 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Gatekeeper assessment"
|
|
||||||
spctl -a -vv --type execute "$app_path" || true
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "Done."
|
|
||||||
echo "App: $app_path"
|
|
||||||
echo "Zip: $zip_path"
|
|
||||||
if [[ -n "${dmg_path}" ]]; then
|
|
||||||
echo "DMG: $dmg_path"
|
|
||||||
fi
|
|
||||||
|
|
||||||
@@ -1,207 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# macOS signing identity sanity check for:
|
|
||||||
# - Developer ID (outside Mac App Store)
|
|
||||||
# - Mac App Store (Apple Distribution + 3rd Party Mac Developer Installer)
|
|
||||||
|
|
||||||
ts() { date +"%H:%M:%S"; }
|
|
||||||
|
|
||||||
if [[ "$(uname -s)" != "Darwin" ]]; then
|
|
||||||
echo "Error: This script is for macOS only." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Strawberry macOS signing identity check"
|
|
||||||
echo "==> [$(ts)] Host: $(sw_vers -productName 2>/dev/null || true) $(sw_vers -productVersion 2>/dev/null || true)"
|
|
||||||
echo
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Keychains searched by 'security' (user)"
|
|
||||||
security list-keychains -d user || true
|
|
||||||
echo
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Valid code signing identities (must include private key)"
|
|
||||||
codesigning_out="$(security find-identity -p codesigning -v 2>&1 || true)"
|
|
||||||
echo "$codesigning_out"
|
|
||||||
echo
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Valid installer/pkg identities (must include private key)"
|
|
||||||
basic_out="$(security find-identity -p basic -v 2>&1 || true)"
|
|
||||||
echo "$basic_out"
|
|
||||||
echo
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Note"
|
|
||||||
cat <<'EOF'
|
|
||||||
- Apple uses multiple certificate types. The "basic" identity list can include certificates that are not usable
|
|
||||||
for signing a Mac App Store upload package.
|
|
||||||
- For App Store Connect uploads via .pkg, you typically need an *Installer* identity (e.g. "3rd Party Mac Developer Installer"
|
|
||||||
or "Mac Installer Distribution") and it must have a private key on this Mac.
|
|
||||||
EOF
|
|
||||||
echo
|
|
||||||
|
|
||||||
list_cert_labels() {
|
|
||||||
local query="$1"
|
|
||||||
# Extract "labl" lines like: "labl"<blob>="Apple Distribution: ..."
|
|
||||||
security find-certificate -a -c "$query" 2>/dev/null \
|
|
||||||
| sed -n 's/.*"labl"<blob>="\(.*\)".*/\1/p' \
|
|
||||||
| sort -u
|
|
||||||
}
|
|
||||||
|
|
||||||
check_label_in_identities() {
|
|
||||||
local label="$1"
|
|
||||||
local out="$2"
|
|
||||||
if echo "$out" | grep -Fq "$label"; then
|
|
||||||
echo "YES"
|
|
||||||
else
|
|
||||||
echo "NO"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
check_label_in_installer_identities() {
|
|
||||||
local label="$1"
|
|
||||||
local out="$2"
|
|
||||||
# Only treat as installer-capable if the cert label itself is an installer cert.
|
|
||||||
case "$label" in
|
|
||||||
*Installer*|*installer*) ;;
|
|
||||||
*) echo "NO"; return 0 ;;
|
|
||||||
esac
|
|
||||||
if echo "$out" | grep -Fq "$label"; then
|
|
||||||
echo "YES"
|
|
||||||
else
|
|
||||||
echo "NO"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
print_section() {
|
|
||||||
local title="$1"
|
|
||||||
shift
|
|
||||||
local queries=("$@")
|
|
||||||
|
|
||||||
echo "==> [$(ts)] ${title}"
|
|
||||||
local any=0
|
|
||||||
|
|
||||||
local q
|
|
||||||
for q in "${queries[@]}"; do
|
|
||||||
local labels
|
|
||||||
labels="$(list_cert_labels "$q" || true)"
|
|
||||||
if [[ -z "$labels" ]]; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
any=1
|
|
||||||
while IFS= read -r label; do
|
|
||||||
[[ -z "$label" ]] && continue
|
|
||||||
local in_codesign in_basic
|
|
||||||
in_codesign="$(check_label_in_identities "$label" "$codesigning_out")"
|
|
||||||
in_basic="$(check_label_in_installer_identities "$label" "$basic_out")"
|
|
||||||
printf -- "- %s\n" "$label"
|
|
||||||
printf -- " - codesigning identity: %s\n" "$in_codesign"
|
|
||||||
printf -- " - installer identity: %s\n" "$in_basic"
|
|
||||||
if [[ "$in_codesign" == "NO" && "$in_basic" == "NO" ]]; then
|
|
||||||
printf -- " - note: certificate exists, but it is NOT a usable identity on this Mac (almost always missing private key)\n"
|
|
||||||
fi
|
|
||||||
done <<<"$labels"
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ "$any" -eq 0 ]]; then
|
|
||||||
echo "(no matching certificates found)"
|
|
||||||
fi
|
|
||||||
echo
|
|
||||||
}
|
|
||||||
|
|
||||||
print_section "Expected for Developer ID (outside Mac App Store)" \
|
|
||||||
"Developer ID Application" \
|
|
||||||
"Developer ID Installer"
|
|
||||||
|
|
||||||
print_section "Expected for Mac App Store submissions" \
|
|
||||||
"Apple Distribution" \
|
|
||||||
"Mac App Distribution" \
|
|
||||||
"3rd Party Mac Developer Application" \
|
|
||||||
"3rd Party Mac Developer Installer" \
|
|
||||||
"Mac Installer Distribution"
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Quick interpretation"
|
|
||||||
cat <<'EOF'
|
|
||||||
- If a certificate label appears above, but both:
|
|
||||||
- codesigning identity: NO
|
|
||||||
- installer identity: NO
|
|
||||||
then the certificate is present but NOT usable for signing on this Mac.
|
|
||||||
The most common cause is: the private key is missing.
|
|
||||||
|
|
||||||
Fix:
|
|
||||||
- Open Keychain Access → login → "My Certificates"
|
|
||||||
- Expand the certificate. You must see a private key underneath it.
|
|
||||||
- If there is no private key:
|
|
||||||
- Recreate the certificate on this Mac via Xcode (Accounts → Manage Certificates), OR
|
|
||||||
- Import a .p12 that includes the private key from the machine where it was created.
|
|
||||||
EOF
|
|
||||||
echo
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Provisioning profiles (Mac App Store builds require one)"
|
|
||||||
prof_dirs=(
|
|
||||||
"${HOME}/Library/Developer/Xcode/UserData/Provisioning Profiles"
|
|
||||||
"${HOME}/Library/MobileDevice/Provisioning Profiles"
|
|
||||||
)
|
|
||||||
any_prof=0
|
|
||||||
for prof_dir in "${prof_dirs[@]}"; do
|
|
||||||
if [[ -d "${prof_dir}" ]]; then
|
|
||||||
any_prof=1
|
|
||||||
echo " ${prof_dir}"
|
|
||||||
ls -la "${prof_dir}" | head -n 20
|
|
||||||
echo
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
if [[ "$any_prof" -eq 0 ]]; then
|
|
||||||
echo "(no provisioning profile directories found in common locations)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Tip: to pick the right MAS profile for a bundle id, run:"
|
|
||||||
echo " ./build_tools/macos/find_mas_provisioning_profile.sh --bundle-id com.dryark.strawberry"
|
|
||||||
\n\necho\n
|
|
||||||
echo "==> [$(ts)] Recommended SHA-1 values to use (avoids ambiguity when names are duplicated)"
|
|
||||||
cat <<'EOF'
|
|
||||||
When you have multiple identities with the same display name, prefer using the SHA-1 hash in scripts:
|
|
||||||
|
|
||||||
--codesign-identity "<SHA1>"
|
|
||||||
--installer-identity "<SHA1>"
|
|
||||||
|
|
||||||
This prevents codesign/productbuild from picking an unexpected identity.
|
|
||||||
EOF
|
|
||||||
echo
|
|
||||||
|
|
||||||
extract_identities() {
|
|
||||||
local policy="$1" # codesigning | basic
|
|
||||||
# Output: SHA1|LABEL
|
|
||||||
security find-identity -p "$policy" -v 2>/dev/null \
|
|
||||||
| sed -n 's/^[[:space:]]*[0-9][0-9]*[)] \([0-9A-F]\{40\}\) "\(.*\)"$/\1|\2/p'
|
|
||||||
}
|
|
||||||
|
|
||||||
print_sha_list() {
|
|
||||||
local title="$1"
|
|
||||||
local policy="$2"
|
|
||||||
local label_match="$3"
|
|
||||||
|
|
||||||
echo "$title"
|
|
||||||
local matches
|
|
||||||
matches="$(extract_identities "$policy" | grep -F "$label_match" || true)"
|
|
||||||
if [[ -z "$matches" ]]; then
|
|
||||||
echo " (none found)"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
local first=1
|
|
||||||
while IFS='|' read -r sha label; do
|
|
||||||
[[ -z "$sha" || -z "$label" ]] && continue
|
|
||||||
if [[ $first -eq 1 ]]; then
|
|
||||||
echo " recommended: $sha ($label)"
|
|
||||||
first=0
|
|
||||||
else
|
|
||||||
echo " alternative: $sha ($label)"
|
|
||||||
fi
|
|
||||||
done <<<"$matches"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_sha_list "Mac App Store (app signing) [use with --codesign-identity]:" "codesigning" "Apple Distribution:"
|
|
||||||
print_sha_list "Mac App Store (pkg signing) [use with --installer-identity]:" "basic" "3rd Party Mac Developer Installer:"
|
|
||||||
print_sha_list "Developer ID (app signing) [outside App Store]:" "codesigning" "Developer ID Application:"
|
|
||||||
print_sha_list "Developer ID (pkg signing) [outside App Store]:" "basic" "Developer ID Installer:"
|
|
||||||
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
Encryption Export Compliance Statement (EAR)
|
|
||||||
|
|
||||||
App Name: Strawberry
|
|
||||||
Bundle ID: com.dryark.strawberry
|
|
||||||
Version: 0.0.0
|
|
||||||
Developer: Dry Ark LLC
|
|
||||||
Date: 2026-01-22
|
|
||||||
Contact: support@example.com
|
|
||||||
|
|
||||||
Statement
|
|
||||||
---------
|
|
||||||
This app uses encryption only for standard network communications security (for example, TLS/HTTPS connections to web services).
|
|
||||||
|
|
||||||
The app does not implement proprietary or non-standard encryption algorithms, and it does not ship its own cryptographic library in place of Apple’s operating system encryption.
|
|
||||||
|
|
||||||
The app uses only encryption provided by Apple’s operating system (e.g., Apple-provided TLS stacks accessed through system frameworks used by the app and its dependencies).
|
|
||||||
|
|
||||||
The app is not a VPN client/server, does not provide end-to-end encrypted messaging, and does not provide user-controlled key management or custom cryptographic functionality beyond standard transport security.
|
|
||||||
|
|
||||||
Accordingly, the app’s use of encryption is believed to qualify as exempt under U.S. export regulations for mass-market software using standard OS-provided encryption.
|
|
||||||
|
|
||||||
Reference
|
|
||||||
---------
|
|
||||||
Apple: Complying with Encryption Export Regulations
|
|
||||||
https://developer.apple.com/documentation/security/complying-with-encryption-export-regulations
|
|
||||||
|
|
||||||
Binary file not shown.
@@ -1,26 +0,0 @@
|
|||||||
Encryption Export Compliance Statement (EAR)
|
|
||||||
|
|
||||||
App Name: Strawberry
|
|
||||||
Bundle ID: @BUNDLE_ID@
|
|
||||||
Version: @VERSION@
|
|
||||||
Developer: @DEVELOPER@
|
|
||||||
Date: @DATE@
|
|
||||||
Contact: @CONTACT@
|
|
||||||
|
|
||||||
Statement
|
|
||||||
---------
|
|
||||||
This app uses encryption only for standard network communications security (for example, TLS/HTTPS connections to web services).
|
|
||||||
|
|
||||||
The app does not implement proprietary or non-standard encryption algorithms, and it does not ship its own cryptographic library in place of Apple’s operating system encryption.
|
|
||||||
|
|
||||||
The app uses only encryption provided by Apple’s operating system (e.g., Apple-provided TLS stacks accessed through system frameworks used by the app and its dependencies).
|
|
||||||
|
|
||||||
The app is not a VPN client/server, does not provide end-to-end encrypted messaging, and does not provide user-controlled key management or custom cryptographic functionality beyond standard transport security.
|
|
||||||
|
|
||||||
Accordingly, the app’s use of encryption is believed to qualify as exempt under U.S. export regulations for mass-market software using standard OS-provided encryption.
|
|
||||||
|
|
||||||
Reference
|
|
||||||
---------
|
|
||||||
Apple: Complying with Encryption Export Regulations
|
|
||||||
https://developer.apple.com/documentation/security/complying-with-encryption-export-regulations
|
|
||||||
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
# Export compliance (encryption)
|
|
||||||
|
|
||||||
Apple may require an "Export Compliance" statement upload when submitting to the Mac App Store.
|
|
||||||
|
|
||||||
This folder contains:
|
|
||||||
|
|
||||||
- `EXPORT_COMPLIANCE.txt`: a template statement (fill-in placeholders)
|
|
||||||
- `make_pdf.sh`: a helper to fill the template and generate a PDF you can upload
|
|
||||||
|
|
||||||
## Generate the PDF
|
|
||||||
|
|
||||||
From the repo root:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./build_tools/macos/export_compliance/make_pdf.sh \
|
|
||||||
--bundle-id com.dryark.strawberry \
|
|
||||||
--version 1.2.3 \
|
|
||||||
--developer "Dry Ark LLC" \
|
|
||||||
--contact "support@example.com"
|
|
||||||
```
|
|
||||||
|
|
||||||
Outputs:
|
|
||||||
|
|
||||||
- `build_tools/macos/export_compliance/EXPORT_COMPLIANCE.filled.txt`
|
|
||||||
- `build_tools/macos/export_compliance/EXPORT_COMPLIANCE.pdf`
|
|
||||||
|
|
||||||
## Important
|
|
||||||
|
|
||||||
This template assumes the app uses **only standard OS-provided encryption** (e.g. TLS/HTTPS via system frameworks) and does **not** ship proprietary or standalone crypto libraries.
|
|
||||||
|
|
||||||
If you bundle your own crypto library (e.g. OpenSSL) or implement custom encryption, you likely need different answers/documentation.
|
|
||||||
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'EOF'
|
|
||||||
Usage:
|
|
||||||
./build_tools/macos/export_compliance/make_pdf.sh \
|
|
||||||
--bundle-id com.dryark.strawberry \
|
|
||||||
--version 1.2.3 \
|
|
||||||
--developer "Dry Ark LLC" \
|
|
||||||
--contact "support@example.com"
|
|
||||||
|
|
||||||
Outputs (in the same folder as this script):
|
|
||||||
- EXPORT_COMPLIANCE.filled.txt
|
|
||||||
- EXPORT_COMPLIANCE.pdf
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
- Uses macOS built-in /usr/sbin/cupsfilter to generate the PDF from plain text.
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
template="${script_dir}/EXPORT_COMPLIANCE.txt"
|
|
||||||
filled="${script_dir}/EXPORT_COMPLIANCE.filled.txt"
|
|
||||||
pdf="${script_dir}/EXPORT_COMPLIANCE.pdf"
|
|
||||||
tmp_html="${script_dir}/EXPORT_COMPLIANCE.tmp.html"
|
|
||||||
|
|
||||||
bundle_id=""
|
|
||||||
version=""
|
|
||||||
developer=""
|
|
||||||
contact=""
|
|
||||||
date_str="$(date +%Y-%m-%d)"
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--bundle-id) bundle_id="${2:-}"; shift 2 ;;
|
|
||||||
--version) version="${2:-}"; shift 2 ;;
|
|
||||||
--developer) developer="${2:-}"; shift 2 ;;
|
|
||||||
--contact) contact="${2:-}"; shift 2 ;;
|
|
||||||
-h|--help) usage; exit 0 ;;
|
|
||||||
*) echo "Unknown arg: $1" >&2; usage; exit 2 ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ -z "$bundle_id" || -z "$version" || -z "$developer" || -z "$contact" ]]; then
|
|
||||||
echo "Error: missing required args." >&2
|
|
||||||
usage
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$(uname -s)" != "Darwin" ]]; then
|
|
||||||
echo "Error: This script is for macOS only (uses textutil)." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ ! -f "$template" ]]; then
|
|
||||||
echo "Error: missing template file: $template" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ ! -x /usr/sbin/cupsfilter ]]; then
|
|
||||||
echo "Error: /usr/sbin/cupsfilter not found. This should exist on macOS." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
escape_sed_repl() {
|
|
||||||
# Escape characters that are special in sed replacement strings: \ & and delimiter |
|
|
||||||
# bash 3.2 compatible
|
|
||||||
echo "$1" | sed -e 's/[\\&|]/\\&/g'
|
|
||||||
}
|
|
||||||
|
|
||||||
bundle_id_esc="$(escape_sed_repl "$bundle_id")"
|
|
||||||
version_esc="$(escape_sed_repl "$version")"
|
|
||||||
developer_esc="$(escape_sed_repl "$developer")"
|
|
||||||
contact_esc="$(escape_sed_repl "$contact")"
|
|
||||||
date_esc="$(escape_sed_repl "$date_str")"
|
|
||||||
|
|
||||||
sed \
|
|
||||||
-e "s|@BUNDLE_ID@|${bundle_id_esc}|g" \
|
|
||||||
-e "s|@VERSION@|${version_esc}|g" \
|
|
||||||
-e "s|@DEVELOPER@|${developer_esc}|g" \
|
|
||||||
-e "s|@CONTACT@|${contact_esc}|g" \
|
|
||||||
-e "s|@DATE@|${date_esc}|g" \
|
|
||||||
"$template" > "$filled"
|
|
||||||
|
|
||||||
rm -f "$pdf" >/dev/null 2>&1 || true
|
|
||||||
rm -f "$tmp_html" >/dev/null 2>&1 || true
|
|
||||||
|
|
||||||
# Convert plain text to PDF. cupsfilter writes PDF to stdout.
|
|
||||||
# Suppress noisy DEBUG output from cupsfilter on stderr.
|
|
||||||
/usr/sbin/cupsfilter -i text/plain -m application/pdf "$filled" > "$pdf" 2>/dev/null
|
|
||||||
|
|
||||||
echo "Wrote:"
|
|
||||||
echo " $filled"
|
|
||||||
echo " $pdf"
|
|
||||||
|
|
||||||
@@ -1,210 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import argparse
|
|
||||||
import datetime as dt
|
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
|
||||||
|
|
||||||
import plistlib
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ProfileInfo:
|
|
||||||
path: Path
|
|
||||||
uuid: str
|
|
||||||
name: str
|
|
||||||
team_id: str
|
|
||||||
expiration: Optional[dt.datetime]
|
|
||||||
app_id: str
|
|
||||||
platforms: List[str]
|
|
||||||
|
|
||||||
|
|
||||||
def run(cmd: List[str]) -> Tuple[int, bytes, bytes]:
|
|
||||||
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
||||||
out, err = p.communicate()
|
|
||||||
return p.returncode, out, err
|
|
||||||
|
|
||||||
|
|
||||||
def decode_profile_plist_bytes(profile_path: Path) -> Optional[bytes]:
|
|
||||||
# Provisioning profiles are typically CMS/PKCS#7 SignedData blobs whose payload is a plist.
|
|
||||||
# However, some tools store them as plain XML plists already. Also, LibreSSL/OpenSSL behavior
|
|
||||||
# differs: LibreSSL usually requires an explicit '-verify' to emit the embedded content.
|
|
||||||
data = profile_path.read_bytes()
|
|
||||||
|
|
||||||
# Fast path: already a plist (XML).
|
|
||||||
if b"<plist" in data:
|
|
||||||
return data
|
|
||||||
|
|
||||||
# Decode CMS/PKCS7 to extract embedded plist payload.
|
|
||||||
# Try a small matrix of commands/inform formats for compatibility.
|
|
||||||
candidates: List[List[str]] = []
|
|
||||||
for inform in ("DER", "PEM"):
|
|
||||||
candidates.append(["/usr/bin/openssl", "cms", "-verify", "-noverify", "-inform", inform, "-in", str(profile_path)])
|
|
||||||
candidates.append(["/usr/bin/openssl", "smime", "-verify", "-noverify", "-inform", inform, "-in", str(profile_path)])
|
|
||||||
|
|
||||||
for cmd in candidates:
|
|
||||||
rc, out, _err = run(cmd)
|
|
||||||
if rc == 0 and b"<plist" in out:
|
|
||||||
return out
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def parse_plist(plist_bytes: bytes) -> Dict[str, Any]:
|
|
||||||
return plistlib.loads(plist_bytes)
|
|
||||||
|
|
||||||
|
|
||||||
def iso(dt_obj: Optional[dt.datetime]) -> str:
|
|
||||||
if not dt_obj:
|
|
||||||
return "(unknown)"
|
|
||||||
# Force UTC-ish display if tz-aware, otherwise as-is.
|
|
||||||
try:
|
|
||||||
return dt_obj.isoformat().replace("+00:00", "Z")
|
|
||||||
except Exception:
|
|
||||||
return str(dt_obj)
|
|
||||||
|
|
||||||
|
|
||||||
def safe_str(v: Any) -> str:
|
|
||||||
if v is None:
|
|
||||||
return ""
|
|
||||||
if isinstance(v, bytes):
|
|
||||||
try:
|
|
||||||
return v.decode("utf-8", errors="replace")
|
|
||||||
except Exception:
|
|
||||||
return repr(v)
|
|
||||||
return str(v)
|
|
||||||
|
|
||||||
|
|
||||||
def profile_info_from_plist(path: Path, p: Dict[str, Any]) -> ProfileInfo:
|
|
||||||
uuid = safe_str(p.get("UUID", "")) or "(unknown)"
|
|
||||||
name = safe_str(p.get("Name", "")) or "(unknown)"
|
|
||||||
team_ids = p.get("TeamIdentifier") or []
|
|
||||||
team_id = safe_str(team_ids[0]) if isinstance(team_ids, list) and team_ids else ""
|
|
||||||
if not team_id:
|
|
||||||
prefixes = p.get("ApplicationIdentifierPrefix") or []
|
|
||||||
team_id = safe_str(prefixes[0]) if isinstance(prefixes, list) and prefixes else "(unknown)"
|
|
||||||
exp = p.get("ExpirationDate")
|
|
||||||
expiration = exp if isinstance(exp, dt.datetime) else None
|
|
||||||
ent = p.get("Entitlements") or {}
|
|
||||||
app_id = safe_str(ent.get("application-identifier") or ent.get("com.apple.application-identifier") or "") or "(unknown)"
|
|
||||||
platforms = p.get("Platform") or []
|
|
||||||
if isinstance(platforms, str):
|
|
||||||
platforms = [platforms]
|
|
||||||
platforms = [safe_str(x) for x in platforms if x is not None]
|
|
||||||
return ProfileInfo(path=path, uuid=uuid, name=name, team_id=team_id or "(unknown)", expiration=expiration, app_id=app_id, platforms=platforms)
|
|
||||||
|
|
||||||
|
|
||||||
def score(profile: ProfileInfo, bundle_id: str, now: dt.datetime) -> Tuple[int, str]:
|
|
||||||
# Prefer non-expired.
|
|
||||||
if profile.expiration and profile.expiration < now:
|
|
||||||
return (-1, "expired")
|
|
||||||
|
|
||||||
score = 0
|
|
||||||
reason = []
|
|
||||||
|
|
||||||
# Prefer exact app id match TEAMID.bundle_id
|
|
||||||
if profile.team_id != "(unknown)" and profile.app_id != "(unknown)":
|
|
||||||
exact = f"{profile.team_id}.{bundle_id}"
|
|
||||||
if profile.app_id == exact:
|
|
||||||
score += 100
|
|
||||||
reason.append(f"exact {profile.app_id}")
|
|
||||||
elif profile.app_id.endswith(f".{bundle_id}"):
|
|
||||||
score += 60
|
|
||||||
reason.append(f"endswith {profile.app_id}")
|
|
||||||
elif "*" in profile.app_id and profile.app_id.startswith(f"{profile.team_id}."):
|
|
||||||
score += 40
|
|
||||||
reason.append(f"wildcard {profile.app_id}")
|
|
||||||
|
|
||||||
# Heuristic: name suggests MAS.
|
|
||||||
n = profile.name.lower()
|
|
||||||
if "mac app store" in n or "app store" in n or "appstore" in n:
|
|
||||||
score += 5
|
|
||||||
reason.append("name looks like MAS")
|
|
||||||
|
|
||||||
# Prefer macOS platform if present.
|
|
||||||
plats = [p.lower() for p in profile.platforms]
|
|
||||||
if any("macos" in p for p in plats):
|
|
||||||
score += 2
|
|
||||||
reason.append("platform macos")
|
|
||||||
|
|
||||||
return (score, ", ".join(reason) if reason else "")
|
|
||||||
|
|
||||||
|
|
||||||
def find_profiles() -> List[Path]:
|
|
||||||
dirs = [
|
|
||||||
Path.home() / "Library" / "Developer" / "Xcode" / "UserData" / "Provisioning Profiles",
|
|
||||||
Path.home() / "Library" / "MobileDevice" / "Provisioning Profiles",
|
|
||||||
]
|
|
||||||
out: List[Path] = []
|
|
||||||
for d in dirs:
|
|
||||||
if not d.is_dir():
|
|
||||||
continue
|
|
||||||
for p in d.iterdir():
|
|
||||||
if p.is_file() and (p.name.endswith(".provisionprofile") or p.name.endswith(".mobileprovision")):
|
|
||||||
out.append(p)
|
|
||||||
return sorted(out)
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
ap = argparse.ArgumentParser()
|
|
||||||
ap.add_argument("--bundle-id", required=True)
|
|
||||||
args = ap.parse_args()
|
|
||||||
bundle_id = args.bundle_id
|
|
||||||
|
|
||||||
if not Path("/usr/bin/openssl").exists():
|
|
||||||
print("Error: /usr/bin/openssl not found.", file=sys.stderr)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
candidates = find_profiles()
|
|
||||||
if not candidates:
|
|
||||||
print("No provisioning profiles found in common locations.", file=sys.stderr)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
print(f"Scanning {len(candidates)} provisioning profile(s) for bundle id: {bundle_id}")
|
|
||||||
print()
|
|
||||||
print(f"{'No.':<4} {'UUID':<36} {'TeamID':<10} {'Expires':<25} {'AppID':<45} Path")
|
|
||||||
print(f"{'-'*4} {'-'*36} {'-'*10} {'-'*25} {'-'*45} ----")
|
|
||||||
|
|
||||||
infos: List[ProfileInfo] = []
|
|
||||||
for i, p in enumerate(candidates, start=1):
|
|
||||||
plist_bytes = decode_profile_plist_bytes(p)
|
|
||||||
if not plist_bytes:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
pl = parse_plist(plist_bytes)
|
|
||||||
info = profile_info_from_plist(p, pl)
|
|
||||||
infos.append(info)
|
|
||||||
print(f"{i:<4} {info.uuid:<36} {info.team_id:<10} {iso(info.expiration):<25} {info.app_id:<45} {info.path}")
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not infos:
|
|
||||||
print("\nCould not decode any provisioning profiles with openssl cms.", file=sys.stderr)
|
|
||||||
return 2
|
|
||||||
|
|
||||||
now = dt.datetime.now(dt.timezone.utc)
|
|
||||||
best: Optional[Tuple[int, str, ProfileInfo]] = None
|
|
||||||
for info in infos:
|
|
||||||
sc, why = score(info, bundle_id, now)
|
|
||||||
if best is None or sc > best[0]:
|
|
||||||
best = (sc, why, info)
|
|
||||||
|
|
||||||
print()
|
|
||||||
if best is None or best[0] <= 0:
|
|
||||||
print(f"Could not confidently auto-select a profile for {bundle_id}.", file=sys.stderr)
|
|
||||||
print("Pick the profile whose AppID is TEAMID.<bundle-id> and is a macOS Mac App Store profile.", file=sys.stderr)
|
|
||||||
return 2
|
|
||||||
|
|
||||||
_, why, info = best
|
|
||||||
print("Recommended profile:")
|
|
||||||
print(f" {info.path}")
|
|
||||||
print(f" reason: {why}")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
raise SystemExit(main())
|
|
||||||
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
ts() { date +"%H:%M:%S"; }
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'EOF'
|
|
||||||
Usage:
|
|
||||||
./build_tools/macos/find_mas_provisioning_profile.sh --bundle-id com.dryark.strawberry
|
|
||||||
|
|
||||||
What it does:
|
|
||||||
- Scans common macOS provisioning profile locations (new Xcode + legacy)
|
|
||||||
- Uses Apple's `security cms -D` to decode each *.provisionprofile into a plist
|
|
||||||
- Prints a readable table and recommends a best match for the given bundle id
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
- A provisioning profile is required for Mac App Store signing.
|
|
||||||
- This script only helps you *find* the right profile file.
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
if [[ "$(uname -s)" != "Darwin" ]]; then
|
|
||||||
echo "Error: This script is for macOS only." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
bundle_id=""
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--bundle-id) bundle_id="${2:-}"; shift 2 ;;
|
|
||||||
-h|--help) usage; exit 0 ;;
|
|
||||||
*) echo "Unknown arg: $1" >&2; usage; exit 2 ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ -z "$bundle_id" ]]; then
|
|
||||||
echo "Error: missing --bundle-id" >&2
|
|
||||||
usage
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! command -v security >/dev/null 2>&1; then
|
|
||||||
echo "Error: 'security' not found." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
plistbuddy_print() {
|
|
||||||
local keypath="$1"
|
|
||||||
local plist="$2"
|
|
||||||
/usr/libexec/PlistBuddy -c "Print :${keypath}" "$plist" 2>/dev/null || true
|
|
||||||
}
|
|
||||||
|
|
||||||
plutil_extract() {
|
|
||||||
local keypath="$1"
|
|
||||||
local plist="$2"
|
|
||||||
/usr/bin/plutil -extract "$keypath" raw -o - "$plist" 2>/dev/null || true
|
|
||||||
}
|
|
||||||
|
|
||||||
find_profiles_in_dir() {
|
|
||||||
local dir="$1"
|
|
||||||
if [[ -d "$dir" ]]; then
|
|
||||||
find "$dir" -maxdepth 1 -type f \( -name "*.provisionprofile" -o -name "*.mobileprovision" \) 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
declare -a candidates
|
|
||||||
candidates=()
|
|
||||||
while IFS= read -r f; do candidates+=("$f"); done < <(find_profiles_in_dir "$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles")
|
|
||||||
while IFS= read -r f; do candidates+=("$f"); done < <(find_profiles_in_dir "$HOME/Library/MobileDevice/Provisioning Profiles")
|
|
||||||
|
|
||||||
if [[ ${#candidates[@]} -eq 0 ]]; then
|
|
||||||
echo "==> [$(ts)] No provisioning profiles found in common locations."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Scanning ${#candidates[@]} provisioning profile(s) for bundle id: ${bundle_id}"
|
|
||||||
echo
|
|
||||||
printf "%-4s %-36s %-10s %-25s %-45s %s\n" "No." "UUID" "TeamID" "Expires" "AppID" "Path"
|
|
||||||
printf "%s\n" "---- ------------------------------------ ---------- ------------------------- --------------------------------------------- ----"
|
|
||||||
|
|
||||||
best_score=-1
|
|
||||||
best_path=""
|
|
||||||
best_reason=""
|
|
||||||
|
|
||||||
idx=0
|
|
||||||
for f in "${candidates[@]}"; do
|
|
||||||
idx=$((idx + 1))
|
|
||||||
|
|
||||||
tmp="$(mktemp -t strawberry-profile.XXXXXX.plist)"
|
|
||||||
if ! security cms -D -i "$f" >"$tmp" 2>/dev/null; then
|
|
||||||
rm -f "$tmp" >/dev/null 2>&1 || true
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
uuid="$(plutil_extract UUID "$tmp")"
|
|
||||||
name="$(plutil_extract Name "$tmp")"
|
|
||||||
teamid="$(plutil_extract 'TeamIdentifier.0' "$tmp")"
|
|
||||||
if [[ -z "$teamid" ]]; then
|
|
||||||
teamid="$(plutil_extract 'ApplicationIdentifierPrefix.0' "$tmp")"
|
|
||||||
fi
|
|
||||||
exp="$(plutil_extract ExpirationDate "$tmp")"
|
|
||||||
|
|
||||||
# App identifier lives under Entitlements; use PlistBuddy because some key names contain dots.
|
|
||||||
appid="$(plistbuddy_print 'Entitlements:application-identifier' "$tmp")"
|
|
||||||
if [[ -z "$appid" ]]; then
|
|
||||||
appid="$(plistbuddy_print 'Entitlements:com.apple.application-identifier' "$tmp")"
|
|
||||||
fi
|
|
||||||
|
|
||||||
rm -f "$tmp" >/dev/null 2>&1 || true
|
|
||||||
|
|
||||||
[[ -z "$uuid" ]] && uuid="(unknown)"
|
|
||||||
[[ -z "$teamid" ]] && teamid="(unknown)"
|
|
||||||
[[ -z "$exp" ]] && exp="(unknown)"
|
|
||||||
[[ -z "$appid" ]] && appid="(unknown)"
|
|
||||||
|
|
||||||
printf "%-4s %-36s %-10s %-25s %-45s %s\n" "$idx" "$uuid" "$teamid" "$exp" "$appid" "$f"
|
|
||||||
|
|
||||||
score=0
|
|
||||||
reason=""
|
|
||||||
|
|
||||||
if [[ "$appid" != "(unknown)" && "$teamid" != "(unknown)" ]]; then
|
|
||||||
if [[ "$appid" == "${teamid}.${bundle_id}" ]]; then
|
|
||||||
score=100
|
|
||||||
reason="exact match (${appid})"
|
|
||||||
elif [[ "$appid" == *".${bundle_id}" ]]; then
|
|
||||||
score=50
|
|
||||||
reason="endswith match (${appid})"
|
|
||||||
elif [[ "$appid" == "${teamid}."* && "$appid" == *"*"* ]]; then
|
|
||||||
score=40
|
|
||||||
reason="wildcard match (${appid})"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$score" -gt 0 && -n "$name" ]]; then
|
|
||||||
case "$name" in
|
|
||||||
*Mac\ App\ Store*|*App\ Store*|*appstore*|*AppStore*)
|
|
||||||
score=$((score + 5))
|
|
||||||
reason="${reason}, name looks like MAS"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$score" -gt "$best_score" ]]; then
|
|
||||||
best_score="$score"
|
|
||||||
best_path="$f"
|
|
||||||
best_reason="$reason"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo
|
|
||||||
if [[ "$best_score" -le 0 ]]; then
|
|
||||||
echo "==> [$(ts)] Could not confidently auto-select a profile for ${bundle_id}."
|
|
||||||
echo "Pick the profile whose AppID is TEAMID.${bundle_id} and is a macOS Mac App Store profile."
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Recommended profile:"
|
|
||||||
echo " $best_path"
|
|
||||||
echo " reason: $best_reason"
|
|
||||||
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
repo_root="$(cd -- "${script_dir}/../.." && pwd)"
|
|
||||||
|
|
||||||
ts() { date +"%H:%M:%S"; }
|
|
||||||
|
|
||||||
run_with_heartbeat() {
|
|
||||||
local desc="$1"
|
|
||||||
shift
|
|
||||||
|
|
||||||
local start now elapsed hb_pid
|
|
||||||
start="$(date +%s)"
|
|
||||||
|
|
||||||
echo "==> [$(ts)] ${desc}"
|
|
||||||
|
|
||||||
# Heartbeat: print elapsed time periodically in case the underlying command is quiet
|
|
||||||
(
|
|
||||||
while true; do
|
|
||||||
sleep 20
|
|
||||||
now="$(date +%s)"
|
|
||||||
elapsed="$((now - start))"
|
|
||||||
echo " [$(ts)] ... still working (${elapsed}s elapsed) ..."
|
|
||||||
done
|
|
||||||
) &
|
|
||||||
hb_pid="$!"
|
|
||||||
|
|
||||||
set +e
|
|
||||||
"$@"
|
|
||||||
local rc=$?
|
|
||||||
set -e
|
|
||||||
|
|
||||||
kill "$hb_pid" >/dev/null 2>&1 || true
|
|
||||||
wait "$hb_pid" >/dev/null 2>&1 || true
|
|
||||||
|
|
||||||
now="$(date +%s)"
|
|
||||||
elapsed="$((now - start))"
|
|
||||||
|
|
||||||
if [[ $rc -ne 0 ]]; then
|
|
||||||
echo "Error: '${desc}' failed after ${elapsed}s (exit $rc)." >&2
|
|
||||||
return "$rc"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Done: ${desc} (${elapsed}s)"
|
|
||||||
}
|
|
||||||
|
|
||||||
if [[ "$(uname -s)" != "Darwin" ]]; then
|
|
||||||
echo "Error: This script is for macOS only." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! command -v brew >/dev/null 2>&1; then
|
|
||||||
echo "Error: Homebrew ('brew') not found in PATH." >&2
|
|
||||||
echo "Install Homebrew first: https://brew.sh/" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Brewfile support (`brew bundle`) is built into modern Homebrew. The historical
|
|
||||||
# tap `homebrew/bundle` has been deprecated and may be empty on newer Homebrew.
|
|
||||||
# If `brew bundle` is missing, the fix is to update Homebrew itself.
|
|
||||||
if ! brew bundle --help >/dev/null 2>&1; then
|
|
||||||
run_with_heartbeat "Updating Homebrew (required for 'brew bundle')" bash -lc "brew update"
|
|
||||||
if ! brew bundle --help >/dev/null 2>&1; then
|
|
||||||
echo "Error: This Homebrew installation does not provide 'brew bundle'." >&2
|
|
||||||
echo "Update Homebrew (e.g. 'brew update') or reinstall Homebrew, then re-run this script." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Homebrew taps are git clones; local formula changes must be committed to be visible.
|
|
||||||
if command -v git >/dev/null 2>&1 && git -C "$repo_root" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
||||||
if git -C "$repo_root" status --porcelain Formula/ | grep -q .; then
|
|
||||||
echo "Error: You have uncommitted changes under Formula/." >&2
|
|
||||||
echo "Homebrew taps are git clones, so uncommitted formulae won't be visible to 'brew tap'." >&2
|
|
||||||
echo "Commit your changes, then re-run this script." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Optional: disable auto-update for faster, more predictable runs.
|
|
||||||
export HOMEBREW_NO_AUTO_UPDATE="${HOMEBREW_NO_AUTO_UPDATE:-1}"
|
|
||||||
|
|
||||||
cd "$repo_root"
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Using repo: $repo_root"
|
|
||||||
|
|
||||||
# Strawberry includes local Homebrew formulae under Formula/.
|
|
||||||
# Homebrew requires formulae to be in a tap; we tap this repo via file:// and then
|
|
||||||
# update the tap clone to the latest commit (without untapping, since Homebrew may
|
|
||||||
# refuse to untap when formulae from this tap are installed).
|
|
||||||
run_with_heartbeat "Ensuring local tap exists: strawberry/local" bash -lc \
|
|
||||||
"brew tap | grep -q '^strawberry/local$' || brew tap strawberry/local 'file://$repo_root' >/dev/null"
|
|
||||||
|
|
||||||
run_with_heartbeat "Refreshing strawberry/local tap clone" bash -lc '
|
|
||||||
tap_repo="$(brew --repo strawberry/local)"
|
|
||||||
cd "$tap_repo"
|
|
||||||
# Make sure the remote points at the current local repo path.
|
|
||||||
git remote set-url origin "file://'"$repo_root"'"
|
|
||||||
git fetch -q origin
|
|
||||||
default_ref="$(git symbolic-ref -q --short refs/remotes/origin/HEAD || true)"
|
|
||||||
if [ -z "$default_ref" ]; then
|
|
||||||
default_ref="origin/master"
|
|
||||||
fi
|
|
||||||
git reset --hard -q "$default_ref"
|
|
||||||
|
|
||||||
echo "==> [$(date +\"%H:%M:%S\")] strawberry/local tap repo: $tap_repo"
|
|
||||||
echo " tap HEAD: $(git rev-parse --short HEAD)"
|
|
||||||
echo " origin: $(git remote get-url origin)"
|
|
||||||
# If the source repo is a git repo, also print its HEAD for debugging.
|
|
||||||
if git -C "'"$repo_root"'" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
||||||
echo " src HEAD: $(git -C "'"$repo_root"'" rev-parse --short HEAD)"
|
|
||||||
fi
|
|
||||||
'
|
|
||||||
|
|
||||||
for f in kdsingleapplication-qt6 qtsparkle-qt6 sparkle-framework libgpod macdeploycheck; do
|
|
||||||
if ! info_out="$(brew info "strawberry/local/${f}" 2>&1 >/dev/null)"; then
|
|
||||||
echo "Error: Unable to load formula strawberry/local/${f} from the tapped repo (brew info failed)." >&2
|
|
||||||
echo "Details (brew info):" >&2
|
|
||||||
echo "$info_out" >&2
|
|
||||||
echo "If you recently added/changed formulae, ensure they are committed, then refresh the tap:" >&2
|
|
||||||
echo " git -C \"$(brew --repo strawberry/local)\" pull --ff-only" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
run_with_heartbeat "Installing dependencies from Brewfile" \
|
|
||||||
brew bundle install --file "$repo_root/Brewfile" --verbose
|
|
||||||
|
|
||||||
cat <<EOF
|
|
||||||
|
|
||||||
Done.
|
|
||||||
|
|
||||||
Notes for packaging (optional):
|
|
||||||
- The CMake target 'deploy' expects these env vars for bundling GIO + GStreamer bits:
|
|
||||||
export GIO_EXTRA_MODULES="\$(brew --prefix)/lib/gio/modules"
|
|
||||||
export GST_PLUGIN_SCANNER="\$(brew --prefix gstreamer)/libexec/gstreamer-1.0/gst-plugin-scanner"
|
|
||||||
export GST_PLUGIN_PATH="\$(brew --prefix)/lib/gstreamer-1.0"
|
|
||||||
|
|
||||||
EOF
|
|
||||||
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Create a universal (arm64+x86_64) .app by merging two already-deployed app bundles
|
|
||||||
# that have identical layouts, one built on Apple Silicon and one built on Intel.
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# ./build_tools/macos/make_universal_app.sh \
|
|
||||||
# --arm-app /path/to/arm64/strawberry.app \
|
|
||||||
# --x86-app /path/to/x86_64/strawberry.app \
|
|
||||||
# --out-app /path/to/output/strawberry.app \
|
|
||||||
# [--clean]
|
|
||||||
#
|
|
||||||
# Notes:
|
|
||||||
# - Do NOT sign the per-arch apps first; signatures will be invalidated by lipo anyway.
|
|
||||||
# - Both inputs must be the same app version/config with the same enabled features,
|
|
||||||
# so the file lists match.
|
|
||||||
|
|
||||||
ts() { date +"%H:%M:%S"; }
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'EOF'
|
|
||||||
Usage:
|
|
||||||
./build_tools/macos/make_universal_app.sh --arm-app <path> --x86-app <path> --out-app <path> [--clean]
|
|
||||||
|
|
||||||
What it does:
|
|
||||||
- Copies the arm64 app to --out-app
|
|
||||||
- For every Mach-O file in the copied app, finds the corresponding file in the x86_64 app
|
|
||||||
- Uses lipo to combine the two slices into a universal binary at the same relative path
|
|
||||||
|
|
||||||
Required:
|
|
||||||
--arm-app <path> Path to arm64 Strawberry.app (built+deployed on Apple Silicon)
|
|
||||||
--x86-app <path> Path to x86_64 Strawberry.app (built+deployed on Intel)
|
|
||||||
--out-app <path> Output path for universal Strawberry.app
|
|
||||||
|
|
||||||
Optional:
|
|
||||||
--clean Delete --out-app if it already exists
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
if [[ "$(uname -s)" != "Darwin" ]]; then
|
|
||||||
echo "Error: This script is for macOS only." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
arm_app=""
|
|
||||||
x86_app=""
|
|
||||||
out_app=""
|
|
||||||
do_clean=0
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--arm-app) arm_app="${2:-}"; shift 2 ;;
|
|
||||||
--x86-app) x86_app="${2:-}"; shift 2 ;;
|
|
||||||
--out-app) out_app="${2:-}"; shift 2 ;;
|
|
||||||
--clean) do_clean=1; shift ;;
|
|
||||||
-h|--help) usage; exit 0 ;;
|
|
||||||
*) echo "Unknown arg: $1" >&2; usage; exit 2 ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ -z "$arm_app" || -z "$x86_app" || -z "$out_app" ]]; then
|
|
||||||
echo "Error: missing required args." >&2
|
|
||||||
usage
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
if [[ ! -d "$arm_app" ]]; then
|
|
||||||
echo "Error: --arm-app not found: $arm_app" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
if [[ ! -d "$x86_app" ]]; then
|
|
||||||
echo "Error: --x86-app not found: $x86_app" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
for cmd in /usr/bin/file /usr/bin/lipo /usr/bin/ditto; do
|
|
||||||
if [[ ! -x "$cmd" ]]; then
|
|
||||||
echo "Error: required tool not found: $cmd" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
out_parent="$(cd -- "$(dirname -- "$out_app")" && pwd)"
|
|
||||||
out_name="$(basename -- "$out_app")"
|
|
||||||
out_app="${out_parent}/${out_name}"
|
|
||||||
|
|
||||||
if [[ -e "$out_app" && "$do_clean" -eq 1 ]]; then
|
|
||||||
echo "==> [$(ts)] Removing existing output app: $out_app"
|
|
||||||
rm -rf "$out_app"
|
|
||||||
fi
|
|
||||||
if [[ -e "$out_app" ]]; then
|
|
||||||
echo "Error: output already exists: $out_app (use --clean to overwrite)" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Copying arm64 app to output"
|
|
||||||
/usr/bin/ditto "$arm_app" "$out_app"
|
|
||||||
|
|
||||||
# Remove any existing signatures in the copied app; we'll re-sign after creating universal binaries.
|
|
||||||
echo "==> [$(ts)] Removing existing code signature metadata (will be re-signed later)"
|
|
||||||
find "$out_app" -type d -name "_CodeSignature" -print0 2>/dev/null | while IFS= read -r -d '' d; do
|
|
||||||
rm -rf "$d" || true
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Merging Mach-O files with lipo"
|
|
||||||
|
|
||||||
merged=0
|
|
||||||
skipped=0
|
|
||||||
|
|
||||||
# Traverse output app and lipo-merge any Mach-O file with its counterpart in the x86 app.
|
|
||||||
while IFS= read -r -d '' f; do
|
|
||||||
# Only operate on regular files.
|
|
||||||
if [[ ! -f "$f" ]]; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
ft="$(/usr/bin/file -b "$f" 2>/dev/null || true)"
|
|
||||||
if [[ "$ft" != *"Mach-O"* ]]; then
|
|
||||||
skipped=$((skipped + 1))
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
rel="${f#"$out_app"/}"
|
|
||||||
other="${x86_app}/${rel}"
|
|
||||||
if [[ ! -f "$other" ]]; then
|
|
||||||
echo "Error: missing matching file in x86 app for:" >&2
|
|
||||||
echo " $rel" >&2
|
|
||||||
echo "Expected at:" >&2
|
|
||||||
echo " $other" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
other_ft="$(/usr/bin/file -b "$other" 2>/dev/null || true)"
|
|
||||||
if [[ "$other_ft" != *"Mach-O"* ]]; then
|
|
||||||
echo "Error: file is Mach-O in arm app but not Mach-O in x86 app:" >&2
|
|
||||||
echo " $rel" >&2
|
|
||||||
echo "arm64: $ft" >&2
|
|
||||||
echo "x86_64: $other_ft" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Validate architectures.
|
|
||||||
arm_archs="$(/usr/bin/lipo -archs "$f" 2>/dev/null || true)"
|
|
||||||
x86_archs="$(/usr/bin/lipo -archs "$other" 2>/dev/null || true)"
|
|
||||||
if [[ "$arm_archs" != *"arm64"* ]]; then
|
|
||||||
echo "Error: expected arm64 slice in arm app file:" >&2
|
|
||||||
echo " $rel" >&2
|
|
||||||
echo " archs: $arm_archs" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
if [[ "$x86_archs" != *"x86_64"* ]]; then
|
|
||||||
echo "Error: expected x86_64 slice in x86 app file:" >&2
|
|
||||||
echo " $rel" >&2
|
|
||||||
echo " archs: $x86_archs" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
tmp="$(mktemp -t strawberry-universal.XXXXXX)"
|
|
||||||
/usr/bin/lipo -create "$f" "$other" -output "$tmp"
|
|
||||||
chmod --reference="$f" "$tmp" 2>/dev/null || true
|
|
||||||
mv -f "$tmp" "$f"
|
|
||||||
merged=$((merged + 1))
|
|
||||||
done < <(find "$out_app" -type f -print0 2>/dev/null)
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Done"
|
|
||||||
echo "Merged Mach-O files: $merged"
|
|
||||||
echo "Non-Mach-O files skipped: $skipped"
|
|
||||||
echo "Output app:"
|
|
||||||
echo " $out_app"
|
|
||||||
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
ts() { date +"%H:%M:%S"; }
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'EOF'
|
|
||||||
Usage:
|
|
||||||
./build_tools/macos/print_mas_build_cmd.sh [--bundle-id com.dryark.strawberry] [--profile <path>]
|
|
||||||
|
|
||||||
What it does:
|
|
||||||
- Tries to auto-pick a provisioning profile for the bundle id
|
|
||||||
- Prints an exact build command you can copy/paste for build_mas_pkg.sh
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
- This helper intentionally does NOT try to auto-pick signing identities by parsing Apple tool output.
|
|
||||||
Use SHA-1 identities from:
|
|
||||||
./build_tools/macos/check_signing_identities.sh
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
if [[ "$(uname -s)" != "Darwin" ]]; then
|
|
||||||
echo "Error: This script is for macOS only." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
repo_root="$(cd -- "${script_dir}/../.." && pwd)"
|
|
||||||
|
|
||||||
bundle_id="com.dryark.strawberry"
|
|
||||||
profile_path=""
|
|
||||||
codesign_identity=""
|
|
||||||
installer_identity=""
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--bundle-id) bundle_id="${2:-}"; shift 2 ;;
|
|
||||||
--profile) profile_path="${2:-}"; shift 2 ;;
|
|
||||||
--codesign-identity) codesign_identity="${2:-}"; shift 2 ;;
|
|
||||||
--installer-identity) installer_identity="${2:-}"; shift 2 ;;
|
|
||||||
-h|--help) usage; exit 0 ;;
|
|
||||||
*) echo "Unknown arg: $1" >&2; usage; exit 2 ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ -z "$profile_path" ]]; then
|
|
||||||
# Attempt to auto-select profile using the finder script.
|
|
||||||
finder="${repo_root}/build_tools/macos/find_mas_provisioning_profile.sh"
|
|
||||||
if [[ -x "$finder" ]]; then
|
|
||||||
out="$("$finder" --bundle-id "$bundle_id" 2>/dev/null || true)"
|
|
||||||
# Parse the line after "Recommended profile:"
|
|
||||||
profile_path="$(printf '%s\n' "$out" | awk 'found{print $1; exit} /^Recommended profile:/{found=1} found && $0 ~ /^ \\// {print $1; exit}' | sed 's/^[[:space:]]*//')"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "==> [$(ts)] Recommended build command:"
|
|
||||||
echo
|
|
||||||
echo "./build_tools/macos/build_mas_pkg.sh --run --release --clean \\"
|
|
||||||
echo " --codesign-identity \"${codesign_identity:-<SHA1 from check_signing_identities.sh>}\" \\"
|
|
||||||
echo " --installer-identity \"${installer_identity:-<SHA1 from check_signing_identities.sh>}\" \\"
|
|
||||||
if [[ -n "$profile_path" ]]; then
|
|
||||||
echo " --provisionprofile \"${profile_path}\""
|
|
||||||
else
|
|
||||||
echo " --provisionprofile \"</path/to/profile.provisionprofile>\""
|
|
||||||
echo
|
|
||||||
echo "Note: could not auto-pick a provisioning profile for bundle id '${bundle_id}'."
|
|
||||||
echo "Run:"
|
|
||||||
echo " ./build_tools/macos/find_mas_provisioning_profile.sh --bundle-id ${bundle_id}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
@@ -2,5 +2,5 @@ find_program(LSB_RELEASE_EXEC lsb_release)
|
|||||||
find_program(DPKG_BUILDPACKAGE dpkg-buildpackage)
|
find_program(DPKG_BUILDPACKAGE dpkg-buildpackage)
|
||||||
|
|
||||||
if (LSB_RELEASE_EXEC AND DPKG_BUILDPACKAGE)
|
if (LSB_RELEASE_EXEC AND DPKG_BUILDPACKAGE)
|
||||||
add_custom_target(deb WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMAND ${DPKG_BUILDPACKAGE} -b -d -uc -us -nc -j4)
|
add_custom_target(deb WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} COMMAND ${DPKG_BUILDPACKAGE} -b -d -uc -us)
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -1,114 +1,44 @@
|
|||||||
# NOTE: Packaging helpers should not be REQUIRED at configure time.
|
find_program(MACDEPLOYQT_EXECUTABLE NAMES macdeployqt PATHS /usr/bin /usr/local/bin /opt/local/bin /usr/local/opt/qt6/bin /usr/local/opt/qt5/bin REQUIRED)
|
||||||
# Missing tools should simply disable the related custom targets.
|
|
||||||
find_program(MACDEPLOYQT_EXECUTABLE NAMES macdeployqt PATHS /usr/bin /usr/local/bin /opt/local/bin /usr/local/opt/qt6/bin)
|
|
||||||
if(MACDEPLOYQT_EXECUTABLE)
|
if(MACDEPLOYQT_EXECUTABLE)
|
||||||
message(STATUS "Found macdeployqt: ${MACDEPLOYQT_EXECUTABLE}")
|
message(STATUS "Found macdeployqt: ${MACDEPLOYQT_EXECUTABLE}")
|
||||||
else()
|
else()
|
||||||
message(WARNING "Missing macdeployqt executable.")
|
message(WARNING "Missing macdeployqt executable.")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
find_program(MACDEPLOYCHECK_EXECUTABLE NAMES macdeploycheck PATHS /usr/bin /usr/local/bin /opt/local/bin /usr/local/opt/qt6/bin)
|
find_program(CREATEDMG_EXECUTABLE NAMES create-dmg REQUIRED)
|
||||||
if(MACDEPLOYCHECK_EXECUTABLE)
|
|
||||||
message(STATUS "Found macdeploycheck: ${MACDEPLOYCHECK_EXECUTABLE}")
|
|
||||||
else()
|
|
||||||
message(STATUS "macdeploycheck not found (optional): 'deploycheck' target will be unavailable.")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
find_program(CREATEDMG_EXECUTABLE NAMES create-dmg)
|
|
||||||
if(CREATEDMG_EXECUTABLE)
|
if(CREATEDMG_EXECUTABLE)
|
||||||
message(STATUS "Found create-dmg: ${CREATEDMG_EXECUTABLE}")
|
message(STATUS "Found create-dmg: ${CREATEDMG_EXECUTABLE}")
|
||||||
else()
|
else()
|
||||||
message(WARNING "Missing create-dmg executable.")
|
message(WARNING "Missing create-dmg executable.")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
set(_SPARKLE_FRAMEWORK_DIR "")
|
|
||||||
set(_SPARKLE_ORIGINAL_BIN_LINK "")
|
|
||||||
set(_SPARKLE_ORIGINAL_BIN_REAL "")
|
|
||||||
if(SPARKLE)
|
|
||||||
# SPARKLE may be either the framework directory or the framework binary path.
|
|
||||||
get_filename_component(_sparkle_link "${SPARKLE}" ABSOLUTE)
|
|
||||||
get_filename_component(_sparkle_real "${SPARKLE}" REALPATH)
|
|
||||||
if(_sparkle_link MATCHES "Sparkle\\.framework$")
|
|
||||||
set(_SPARKLE_FRAMEWORK_DIR "${_sparkle_real}")
|
|
||||||
set(_SPARKLE_ORIGINAL_BIN_LINK "${_sparkle_link}/Versions/B/Sparkle")
|
|
||||||
set(_SPARKLE_ORIGINAL_BIN_REAL "${_sparkle_real}/Versions/B/Sparkle")
|
|
||||||
else()
|
|
||||||
# Assume it's the framework binary path:
|
|
||||||
# .../Sparkle.framework/Versions/B/Sparkle
|
|
||||||
set(_SPARKLE_ORIGINAL_BIN_LINK "${_sparkle_link}")
|
|
||||||
set(_SPARKLE_ORIGINAL_BIN_REAL "${_sparkle_real}")
|
|
||||||
get_filename_component(_sparkle_b_dir "${_SPARKLE_ORIGINAL_BIN_REAL}" DIRECTORY) # .../Versions/B
|
|
||||||
get_filename_component(_sparkle_versions_dir "${_sparkle_b_dir}" DIRECTORY) # .../Versions
|
|
||||||
get_filename_component(_SPARKLE_FRAMEWORK_DIR "${_sparkle_versions_dir}" DIRECTORY) # .../Sparkle.framework
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(NOT EXISTS "${_SPARKLE_FRAMEWORK_DIR}" OR NOT EXISTS "${_SPARKLE_ORIGINAL_BIN_REAL}")
|
|
||||||
set(_SPARKLE_FRAMEWORK_DIR "")
|
|
||||||
set(_SPARKLE_ORIGINAL_BIN_LINK "")
|
|
||||||
set(_SPARKLE_ORIGINAL_BIN_REAL "")
|
|
||||||
else()
|
|
||||||
message(STATUS "Sparkle.framework found: ${_SPARKLE_FRAMEWORK_DIR}")
|
|
||||||
endif()
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(MACDEPLOYQT_EXECUTABLE)
|
if(MACDEPLOYQT_EXECUTABLE)
|
||||||
|
|
||||||
# Note: We intentionally do NOT codesign during the CMake 'deploy'/'dmg' targets.
|
if(APPLE_DEVELOPER_ID)
|
||||||
# macdeployqt can optionally sign, but passing identities safely through Ninja's /bin/sh wrapper is brittle.
|
set(MACDEPLOYQT_CODESIGN -codesign=${APPLE_DEVELOPER_ID})
|
||||||
# This repo's signing/notarization pipeline is handled in build_tools/macos/build_sign_notarize.sh instead.
|
set(CREATEDMG_CODESIGN --codesign ${APPLE_DEVELOPER_ID})
|
||||||
|
endif()
|
||||||
if(CREATEDMG_SKIP_JENKINS)
|
if(CREATEDMG_SKIP_JENKINS)
|
||||||
set(CREATEDMG_SKIP_JENKINS_ARG "--skip-jenkins")
|
set(CREATEDMG_SKIP_JENKINS_ARG "--skip-jenkins")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
set(_deploy_commands
|
add_custom_target(deploy
|
||||||
COMMAND mkdir -p ${CMAKE_BINARY_DIR}/strawberry.app/Contents/Frameworks
|
COMMAND mkdir -p ${CMAKE_BINARY_DIR}/strawberry.app/Contents/{Frameworks,Resources}
|
||||||
COMMAND mkdir -p ${CMAKE_BINARY_DIR}/strawberry.app/Contents/Resources
|
COMMAND cp -v ${CMAKE_SOURCE_DIR}/dist/macos/Info.plist ${CMAKE_BINARY_DIR}/strawberry.app/Contents/
|
||||||
COMMAND cp -v ${CMAKE_BINARY_DIR}/dist/macos/Info.plist ${CMAKE_BINARY_DIR}/strawberry.app/Contents/
|
|
||||||
COMMAND cp -v ${CMAKE_SOURCE_DIR}/dist/macos/strawberry.icns ${CMAKE_BINARY_DIR}/strawberry.app/Contents/Resources/
|
COMMAND cp -v ${CMAKE_SOURCE_DIR}/dist/macos/strawberry.icns ${CMAKE_BINARY_DIR}/strawberry.app/Contents/Resources/
|
||||||
)
|
|
||||||
|
|
||||||
if(_SPARKLE_FRAMEWORK_DIR)
|
|
||||||
list(APPEND _deploy_commands
|
|
||||||
COMMAND ${CMAKE_SOURCE_DIR}/dist/macos/bundle_sparkle.sh ${CMAKE_BINARY_DIR}/strawberry.app ${_SPARKLE_FRAMEWORK_DIR} ${_SPARKLE_ORIGINAL_BIN_LINK} ${_SPARKLE_ORIGINAL_BIN_REAL}
|
|
||||||
)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
list(APPEND _deploy_commands
|
|
||||||
COMMAND ${CMAKE_SOURCE_DIR}/dist/macos/macgstcopy.sh ${CMAKE_BINARY_DIR}/strawberry.app
|
COMMAND ${CMAKE_SOURCE_DIR}/dist/macos/macgstcopy.sh ${CMAKE_BINARY_DIR}/strawberry.app
|
||||||
COMMAND ${MACDEPLOYQT_EXECUTABLE} strawberry.app -verbose=3 -executable=${CMAKE_BINARY_DIR}/strawberry.app/Contents/PlugIns/gst-plugin-scanner
|
COMMAND ${MACDEPLOYQT_EXECUTABLE} strawberry.app -verbose=3 -executable=${CMAKE_BINARY_DIR}/strawberry.app/Contents/PlugIns/strawberry-tagreader -executable=${CMAKE_BINARY_DIR}/strawberry.app/Contents/PlugIns/gst-plugin-scanner ${MACDEPLOYQT_CODESIGN}
|
||||||
)
|
|
||||||
|
|
||||||
# Make 'deploy' incremental:
|
|
||||||
# - add_custom_target() is always out-of-date, so it reruns every time.
|
|
||||||
# - using a stamp file makes Ninja/Make skip deploy when inputs haven't changed.
|
|
||||||
set(_deploy_stamp "${CMAKE_BINARY_DIR}/deploy_app_bundle.stamp")
|
|
||||||
|
|
||||||
add_custom_command(
|
|
||||||
OUTPUT "${_deploy_stamp}"
|
|
||||||
${_deploy_commands}
|
|
||||||
COMMAND ${CMAKE_COMMAND} -E touch "${_deploy_stamp}"
|
|
||||||
COMMENT "Deploying app bundle (bundling Sparkle/GStreamer + macdeployqt)"
|
|
||||||
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||||
DEPENDS
|
DEPENDS strawberry strawberry-tagreader
|
||||||
strawberry
|
|
||||||
"${CMAKE_BINARY_DIR}/dist/macos/Info.plist"
|
|
||||||
"${CMAKE_SOURCE_DIR}/dist/macos/strawberry.icns"
|
|
||||||
"${CMAKE_SOURCE_DIR}/dist/macos/macgstcopy.sh"
|
|
||||||
"${CMAKE_SOURCE_DIR}/dist/macos/bundle_sparkle.sh"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
add_custom_target(deploy DEPENDS "${_deploy_stamp}")
|
|
||||||
|
|
||||||
if(MACDEPLOYCHECK_EXECUTABLE)
|
|
||||||
add_custom_target(deploycheck
|
add_custom_target(deploycheck
|
||||||
COMMAND ${MACDEPLOYCHECK_EXECUTABLE} strawberry.app
|
COMMAND ${CMAKE_BINARY_DIR}/ext/macdeploycheck/macdeploycheck strawberry.app
|
||||||
|
DEPENDS macdeploycheck
|
||||||
)
|
)
|
||||||
endif()
|
|
||||||
if(CREATEDMG_EXECUTABLE)
|
if(CREATEDMG_EXECUTABLE)
|
||||||
add_custom_target(dmg
|
add_custom_target(dmg
|
||||||
COMMAND ${CREATEDMG_EXECUTABLE} --volname strawberry --background "${CMAKE_SOURCE_DIR}/dist/macos/dmg_background.png" --app-drop-link 450 218 --icon strawberry.app 150 218 --window-size 600 450 ${CREATEDMG_SKIP_JENKINS_ARG} strawberry-${STRAWBERRY_VERSION_PACKAGE}-${CMAKE_HOST_SYSTEM_PROCESSOR}.dmg strawberry.app
|
COMMAND ${CREATEDMG_EXECUTABLE} --volname strawberry --background "${CMAKE_SOURCE_DIR}/dist/macos/dmg_background.png" --app-drop-link 450 218 --icon strawberry.app 150 218 --window-size 600 450 ${CREATEDMG_CODESIGN} ${CREATEDMG_SKIP_JENKINS_ARG} strawberry-${STRAWBERRY_VERSION_PACKAGE}-${CMAKE_HOST_SYSTEM_PROCESSOR}.dmg strawberry.app
|
||||||
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||||
DEPENDS deploy
|
|
||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
69
cmake/FindCppUnit.cmake
Normal file
69
cmake/FindCppUnit.cmake
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
# - Try to find the libcppunit libraries
|
||||||
|
# Once done this will define
|
||||||
|
#
|
||||||
|
# CppUnit_FOUND - system has libcppunit
|
||||||
|
# CPPUNIT_INCLUDE_DIR - the libcppunit include directory
|
||||||
|
# CPPUNIT_LIBRARIES - libcppunit library
|
||||||
|
|
||||||
|
#include (MacroEnsureVersion)
|
||||||
|
|
||||||
|
if(NOT CPPUNIT_MIN_VERSION)
|
||||||
|
SET(CPPUNIT_MIN_VERSION 1.12.0)
|
||||||
|
endif(NOT CPPUNIT_MIN_VERSION)
|
||||||
|
|
||||||
|
FIND_PROGRAM(CPPUNIT_CONFIG_EXECUTABLE cppunit-config )
|
||||||
|
|
||||||
|
IF(CPPUNIT_INCLUDE_DIR AND CPPUNIT_LIBRARIES)
|
||||||
|
|
||||||
|
# in cache already
|
||||||
|
SET(CppUnit_FOUND TRUE)
|
||||||
|
|
||||||
|
ELSE(CPPUNIT_INCLUDE_DIR AND CPPUNIT_LIBRARIES)
|
||||||
|
|
||||||
|
SET(CPPUNIT_INCLUDE_DIR)
|
||||||
|
SET(CPPUNIT_LIBRARIES)
|
||||||
|
|
||||||
|
IF(CPPUNIT_CONFIG_EXECUTABLE)
|
||||||
|
EXEC_PROGRAM(${CPPUNIT_CONFIG_EXECUTABLE} ARGS --cflags RETURN_VALUE _return_VALUE OUTPUT_VARIABLE CPPUNIT_CFLAGS)
|
||||||
|
EXEC_PROGRAM(${CPPUNIT_CONFIG_EXECUTABLE} ARGS --libs RETURN_VALUE _return_VALUE OUTPUT_VARIABLE CPPUNIT_LIBRARIES)
|
||||||
|
EXEC_PROGRAM(${CPPUNIT_CONFIG_EXECUTABLE} ARGS --version RETURN_VALUE _return_VALUE OUTPUT_VARIABLE CPPUNIT_INSTALLED_VERSION)
|
||||||
|
STRING(REGEX REPLACE "-I(.+)" "\\1" CPPUNIT_CFLAGS "${CPPUNIT_CFLAGS}")
|
||||||
|
ELSE(CPPUNIT_CONFIG_EXECUTABLE)
|
||||||
|
# in case win32 needs to find it the old way?
|
||||||
|
FIND_PATH(CPPUNIT_CFLAGS cppunit/TestRunner.h PATHS /usr/include /usr/local/include )
|
||||||
|
FIND_LIBRARY(CPPUNIT_LIBRARIES NAMES cppunit PATHS /usr/lib /usr/local/lib )
|
||||||
|
# how can we find cppunit version?
|
||||||
|
MESSAGE (STATUS "Ensure you cppunit installed version is at least ${CPPUNIT_MIN_VERSION}")
|
||||||
|
SET (CPPUNIT_INSTALLED_VERSION ${CPPUNIT_MIN_VERSION})
|
||||||
|
ENDIF(CPPUNIT_CONFIG_EXECUTABLE)
|
||||||
|
|
||||||
|
SET(CPPUNIT_INCLUDE_DIR ${CPPUNIT_CFLAGS} "${CPPUNIT_CFLAGS}/cppunit")
|
||||||
|
|
||||||
|
ENDIF(CPPUNIT_INCLUDE_DIR AND CPPUNIT_LIBRARIES)
|
||||||
|
|
||||||
|
IF(CPPUNIT_INCLUDE_DIR AND CPPUNIT_LIBRARIES)
|
||||||
|
|
||||||
|
SET(CppUnit_FOUND TRUE)
|
||||||
|
|
||||||
|
if(NOT CppUnit_FIND_QUIETLY)
|
||||||
|
MESSAGE (STATUS "Found cppunit: ${CPPUNIT_LIBRARIES}")
|
||||||
|
endif(NOT CppUnit_FIND_QUIETLY)
|
||||||
|
|
||||||
|
IF(CPPUNIT_CONFIG_EXECUTABLE)
|
||||||
|
EXEC_PROGRAM(${CPPUNIT_CONFIG_EXECUTABLE} ARGS --version RETURN_VALUE _return_VALUE OUTPUT_VARIABLE CPPUNIT_INSTALLED_VERSION)
|
||||||
|
ENDIF(CPPUNIT_CONFIG_EXECUTABLE)
|
||||||
|
|
||||||
|
#macro_ensure_version( ${CPPUNIT_MIN_VERSION} ${CPPUNIT_INSTALLED_VERSION} CPPUNIT_INSTALLED_VERSION_OK )
|
||||||
|
|
||||||
|
#IF(NOT CPPUNIT_INSTALLED_VERSION_OK)
|
||||||
|
# MESSAGE ("** CppUnit version is too old: found ${CPPUNIT_INSTALLED_VERSION} installed, ${CPPUNIT_MIN_VERSION} or major is required")
|
||||||
|
# SET(CppUnit_FOUND FALSE)
|
||||||
|
#ENDIF(NOT CPPUNIT_INSTALLED_VERSION_OK)
|
||||||
|
|
||||||
|
ELSE(CPPUNIT_INCLUDE_DIR AND CPPUNIT_LIBRARIES)
|
||||||
|
|
||||||
|
SET(CppUnit_FOUND FALSE CACHE BOOL "Not found cppunit library")
|
||||||
|
|
||||||
|
ENDIF(CPPUNIT_INCLUDE_DIR AND CPPUNIT_LIBRARIES)
|
||||||
|
|
||||||
|
MARK_AS_ADVANCED(CPPUNIT_INCLUDE_DIR CPPUNIT_LIBRARIES)
|
||||||
132
cmake/FindFFTW3.cmake
Normal file
132
cmake/FindFFTW3.cmake
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
#
|
||||||
|
# Try to find FFTW3 library
|
||||||
|
# (see www.fftw.org)
|
||||||
|
# Once run this will define:
|
||||||
|
#
|
||||||
|
# FFTW3_FOUND
|
||||||
|
# FFTW3_INCLUDE_DIR
|
||||||
|
# FFTW3_LIBRARIES
|
||||||
|
# FFTW3_LINK_DIRECTORIES
|
||||||
|
#
|
||||||
|
# You may set one of these options before including this file:
|
||||||
|
# FFTW3_USE_SSE2
|
||||||
|
#
|
||||||
|
# TODO: _F_ versions.
|
||||||
|
#
|
||||||
|
# Jan Woetzel 05/2004
|
||||||
|
# www.mip.informatik.uni-kiel.de
|
||||||
|
# --------------------------------
|
||||||
|
|
||||||
|
FIND_PATH(FFTW3_INCLUDE_DIR fftw3.h
|
||||||
|
${FFTW3_DIR}/include
|
||||||
|
${FFTW3_HOME}/include
|
||||||
|
${FFTW3_DIR}
|
||||||
|
${FFTW3_HOME}
|
||||||
|
$ENV{FFTW3_DIR}/include
|
||||||
|
$ENV{FFTW3_HOME}/include
|
||||||
|
$ENV{FFTW3_DIR}
|
||||||
|
$ENV{FFTW3_HOME}
|
||||||
|
/usr/include
|
||||||
|
/usr/local/include
|
||||||
|
$ENV{SOURCE_DIR}/fftw3
|
||||||
|
$ENV{SOURCE_DIR}/fftw3/include
|
||||||
|
$ENV{SOURCE_DIR}/fftw
|
||||||
|
$ENV{SOURCE_DIR}/fftw/include
|
||||||
|
)
|
||||||
|
#MESSAGE("DBG FFTW3_INCLUDE_DIR=${FFTW3_INCLUDE_DIR}")
|
||||||
|
|
||||||
|
|
||||||
|
SET(FFTW3_POSSIBLE_LIBRARY_PATH
|
||||||
|
${FFTW3_DIR}/lib
|
||||||
|
${FFTW3_HOME}/lib
|
||||||
|
${FFTW3_DIR}
|
||||||
|
${FFTW3_HOME}
|
||||||
|
$ENV{FFTW3_DIR}/lib
|
||||||
|
$ENV{FFTW3_HOME}/lib
|
||||||
|
$ENV{FFTW3_DIR}
|
||||||
|
$ENV{FFTW3_HOME}
|
||||||
|
/usr/lib
|
||||||
|
/usr/local/lib
|
||||||
|
$ENV{SOURCE_DIR}/fftw3
|
||||||
|
$ENV{SOURCE_DIR}/fftw3/lib
|
||||||
|
$ENV{SOURCE_DIR}/fftw
|
||||||
|
$ENV{SOURCE_DIR}/fftw/lib
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# The lib prefix is contained in filename of W32, unfortunately. In the "general" lib:
|
||||||
|
FIND_LIBRARY(FFTW3_FFTW_LIBRARY
|
||||||
|
NAMES fftw3 libfftw libfftw3 libfftw3-3
|
||||||
|
PATHS
|
||||||
|
${FFTW3_POSSIBLE_LIBRARY_PATH}
|
||||||
|
)
|
||||||
|
#MESSAGE("DBG FFTW3_FFTW_LIBRARY=${FFTW3_FFTW_LIBRARY}")
|
||||||
|
|
||||||
|
FIND_LIBRARY(FFTW3_FFTWF_LIBRARY
|
||||||
|
NAMES fftwf3 fftw3f fftwf libfftwf libfftwf3 libfftw3f-3
|
||||||
|
PATHS
|
||||||
|
${FFTW3_POSSIBLE_LIBRARY_PATH}
|
||||||
|
)
|
||||||
|
#MESSAGE("DBG FFTW3_FFTWF_LIBRARY=${FFTW3_FFTWF_LIBRARY}")
|
||||||
|
|
||||||
|
FIND_LIBRARY(FFTW3_FFTWL_LIBRARY
|
||||||
|
NAMES fftwl3 fftw3l fftwl libfftwl libfftwl3 libfftw3l-3
|
||||||
|
PATHS
|
||||||
|
${FFTW3_POSSIBLE_LIBRARY_PATH}
|
||||||
|
)
|
||||||
|
#MESSAGE("DBG FFTW3_FFTWF_LIBRARY=${FFTW3_FFTWL_LIBRARY}")
|
||||||
|
|
||||||
|
|
||||||
|
FIND_LIBRARY(FFTW3_FFTW_SSE2_LIBRARY
|
||||||
|
NAMES fftw_sse2 fftw3_sse2 libfftw_sse2 libfftw3_sse2
|
||||||
|
PATHS
|
||||||
|
${FFTW3_POSSIBLE_LIBRARY_PATH}
|
||||||
|
)
|
||||||
|
#MESSAGE("DBG FFTW3_FFTW_SSE2_LIBRARY=${FFTW3_FFTW_SSE2_LIBRARY}")
|
||||||
|
|
||||||
|
FIND_LIBRARY(FFTW3_FFTWF_SSE_LIBRARY
|
||||||
|
NAMES fftwf_sse fftwf3_sse libfftwf_sse libfftwf3_sse
|
||||||
|
PATHS
|
||||||
|
${FFTW3_POSSIBLE_LIBRARY_PATH}
|
||||||
|
)
|
||||||
|
#MESSAGE("DBG FFTW3_FFTWF_SSE_LIBRARY=${FFTW3_FFTWF_SSE_LIBRARY}")
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------
|
||||||
|
# select one of the above
|
||||||
|
# default:
|
||||||
|
IF (FFTW3_FFTW_LIBRARY)
|
||||||
|
SET(FFTW3_LIBRARIES ${FFTW3_FFTW_LIBRARY})
|
||||||
|
ENDIF (FFTW3_FFTW_LIBRARY)
|
||||||
|
# specialized:
|
||||||
|
IF (FFTW3_USE_SSE2 AND FFTW3_FFTW_SSE2_LIBRARY)
|
||||||
|
SET(FFTW3_LIBRARIES ${FFTW3_FFTW_SSE2_LIBRARY})
|
||||||
|
ENDIF (FFTW3_USE_SSE2 AND FFTW3_FFTW_SSE2_LIBRARY)
|
||||||
|
|
||||||
|
# --------------------------------
|
||||||
|
|
||||||
|
IF(FFTW3_LIBRARIES)
|
||||||
|
IF (FFTW3_INCLUDE_DIR)
|
||||||
|
|
||||||
|
# OK, found all we need
|
||||||
|
SET(FFTW3_FOUND TRUE)
|
||||||
|
GET_FILENAME_COMPONENT(FFTW3_LINK_DIRECTORIES ${FFTW3_LIBRARIES} PATH)
|
||||||
|
|
||||||
|
ELSE (FFTW3_INCLUDE_DIR)
|
||||||
|
MESSAGE("FFTW3 include dir not found. Set FFTW3_DIR to find it.")
|
||||||
|
ENDIF(FFTW3_INCLUDE_DIR)
|
||||||
|
ELSE(FFTW3_LIBRARIES)
|
||||||
|
MESSAGE("FFTW3 lib not found. Set FFTW3_DIR to find it.")
|
||||||
|
ENDIF(FFTW3_LIBRARIES)
|
||||||
|
|
||||||
|
|
||||||
|
MARK_AS_ADVANCED(
|
||||||
|
FFTW3_INCLUDE_DIR
|
||||||
|
FFTW3_LIBRARIES
|
||||||
|
FFTW3_FFTW_LIBRARY
|
||||||
|
FFTW3_FFTW_SSE2_LIBRARY
|
||||||
|
FFTW3_FFTWF_LIBRARY
|
||||||
|
FFTW3_FFTWF_SSE_LIBRARY
|
||||||
|
FFTW3_FFTWL_LIBRARY
|
||||||
|
FFTW3_LINK_DIRECTORIES
|
||||||
|
)
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
# Try to find RapidJSON (header-only).
|
|
||||||
#
|
|
||||||
# This project uses `find_package(RapidJSON)` and expects:
|
|
||||||
# - RapidJSON_FOUND
|
|
||||||
# - RapidJSON_INCLUDE_DIRS
|
|
||||||
#
|
|
||||||
# Homebrew's `rapidjson` formula commonly installs headers to:
|
|
||||||
# /opt/homebrew/include/rapidjson
|
|
||||||
# but does not always ship a `RapidJSONConfig.cmake`, so we provide this
|
|
||||||
# Find-module fallback.
|
|
||||||
|
|
||||||
find_path(RapidJSON_INCLUDE_DIR
|
|
||||||
NAMES rapidjson/document.h
|
|
||||||
)
|
|
||||||
|
|
||||||
include(FindPackageHandleStandardArgs)
|
|
||||||
find_package_handle_standard_args(RapidJSON
|
|
||||||
REQUIRED_VARS RapidJSON_INCLUDE_DIR
|
|
||||||
)
|
|
||||||
|
|
||||||
if(RapidJSON_FOUND)
|
|
||||||
set(RapidJSON_INCLUDE_DIRS "${RapidJSON_INCLUDE_DIR}")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(RapidJSON_FOUND AND NOT TARGET RapidJSON::RapidJSON)
|
|
||||||
add_library(RapidJSON::RapidJSON INTERFACE IMPORTED)
|
|
||||||
set_target_properties(RapidJSON::RapidJSON PROPERTIES
|
|
||||||
INTERFACE_INCLUDE_DIRECTORIES "${RapidJSON_INCLUDE_DIR}"
|
|
||||||
)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ if (LSB_RELEASE_EXEC AND RPMBUILD_EXEC)
|
|||||||
add_custom_target(rpm
|
add_custom_target(rpm
|
||||||
COMMAND ${CMAKE_SOURCE_DIR}/dist/scripts/maketarball.sh
|
COMMAND ${CMAKE_SOURCE_DIR}/dist/scripts/maketarball.sh
|
||||||
COMMAND ${CMAKE_COMMAND} -E copy strawberry-${STRAWBERRY_VERSION_PACKAGE}.tar.xz ${RPMBUILD_DIR}/SOURCES/
|
COMMAND ${CMAKE_COMMAND} -E copy strawberry-${STRAWBERRY_VERSION_PACKAGE}.tar.xz ${RPMBUILD_DIR}/SOURCES/
|
||||||
COMMAND ${RPMBUILD_EXEC} -ba ${CMAKE_BINARY_DIR}/strawberry.spec
|
COMMAND ${RPMBUILD_EXEC} -ba ${CMAKE_SOURCE_DIR}/dist/unix/strawberry.spec
|
||||||
)
|
)
|
||||||
|
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -1,32 +1,15 @@
|
|||||||
set(summary_willbuild "")
|
set(summary_willbuild "")
|
||||||
set(summary_willnotbuild "")
|
set(summary_willnotbuild "")
|
||||||
|
|
||||||
# On some platforms (notably macOS via Homebrew), many "optional" dependencies are
|
macro(summary_add name test)
|
||||||
# not installed by default. Historically, Strawberry treated missing optional deps
|
|
||||||
# as a hard error when the option defaulted to ON, which makes first-time builds
|
|
||||||
# frustrating.
|
|
||||||
#
|
|
||||||
# This toggle controls that behavior:
|
|
||||||
# - ON => missing optional deps abort the configure (packager/CI-friendly)
|
|
||||||
# - OFF => missing optional deps auto-disable the component (dev-friendly)
|
|
||||||
set(_optional_components_fatal_default ON)
|
|
||||||
if(APPLE)
|
|
||||||
set(_optional_components_fatal_default OFF)
|
|
||||||
endif()
|
|
||||||
option(OPTIONAL_COMPONENTS_MISSING_DEPS_ARE_FATAL
|
|
||||||
"If ON, missing optional component dependencies are fatal (otherwise components auto-disable)"
|
|
||||||
${_optional_components_fatal_default}
|
|
||||||
)
|
|
||||||
|
|
||||||
macro(optional_component_summary_add name test)
|
|
||||||
if (${test})
|
if (${test})
|
||||||
list(APPEND summary_willbuild ${name})
|
list(APPEND summary_willbuild ${name})
|
||||||
else (${test})
|
else (${test})
|
||||||
list(APPEND summary_willnotbuild "${name}")
|
list(APPEND summary_willnotbuild "${name}")
|
||||||
endif (${test})
|
endif (${test})
|
||||||
endmacro(optional_component_summary_add)
|
endmacro(summary_add)
|
||||||
|
|
||||||
macro(optional_component_summary_show_part variable title)
|
macro(summary_show_part variable title)
|
||||||
list(LENGTH ${variable} _len)
|
list(LENGTH ${variable} _len)
|
||||||
if (_len)
|
if (_len)
|
||||||
message("")
|
message("")
|
||||||
@@ -35,20 +18,19 @@ macro(optional_component_summary_show_part variable title)
|
|||||||
message(" ${_item}")
|
message(" ${_item}")
|
||||||
endforeach (_item)
|
endforeach (_item)
|
||||||
endif (_len)
|
endif (_len)
|
||||||
endmacro(optional_component_summary_show_part)
|
endmacro(summary_show_part)
|
||||||
|
|
||||||
macro(optional_component_summary_show)
|
macro(summary_show)
|
||||||
list(SORT summary_willbuild)
|
list(SORT summary_willbuild)
|
||||||
list(SORT summary_willnotbuild)
|
list(SORT summary_willnotbuild)
|
||||||
message("")
|
message("")
|
||||||
message("Building strawberry version: ${STRAWBERRY_VERSION_DISPLAY}, Qt version ${Qt${QT_VERSION_MAJOR}Core_VERSION}")
|
message("Building strawberry version: ${STRAWBERRY_VERSION_DISPLAY}, Qt version ${Qt${QT_VERSION_MAJOR}Core_VERSION}")
|
||||||
optional_component_summary_show_part(summary_willbuild "The following components will be built:")
|
summary_show_part(summary_willbuild "The following components will be built:")
|
||||||
optional_component_summary_show_part(summary_willnotbuild "The following components WILL NOT be built:")
|
summary_show_part(summary_willnotbuild "The following components WILL NOT be built:")
|
||||||
message("")
|
message("")
|
||||||
endmacro(optional_component_summary_show)
|
endmacro(summary_show)
|
||||||
|
|
||||||
function(optional_component name default description)
|
function(optional_component name default description)
|
||||||
|
|
||||||
set(option_variable "ENABLE_${name}")
|
set(option_variable "ENABLE_${name}")
|
||||||
set(have_variable "HAVE_${name}")
|
set(have_variable "HAVE_${name}")
|
||||||
set(${have_variable} OFF)
|
set(${have_variable} OFF)
|
||||||
@@ -97,14 +79,6 @@ function(optional_component name default description)
|
|||||||
set(text "${description} (missing ${deplist_text})")
|
set(text "${description} (missing ${deplist_text})")
|
||||||
|
|
||||||
set(summary_willnotbuild "${summary_willnotbuild};${text}" PARENT_SCOPE)
|
set(summary_willnotbuild "${summary_willnotbuild};${text}" PARENT_SCOPE)
|
||||||
if(OPTIONAL_COMPONENTS_MISSING_DEPS_ARE_FATAL)
|
|
||||||
message(FATAL_ERROR "${text}, to disable this optional feature, pass -D${option_variable}=OFF to CMake")
|
|
||||||
else()
|
|
||||||
message(STATUS "${text} - disabling ${option_variable}")
|
|
||||||
set(${option_variable} OFF CACHE BOOL "${description}" FORCE)
|
|
||||||
return()
|
|
||||||
endif()
|
|
||||||
|
|
||||||
else()
|
else()
|
||||||
set(${have_variable} ON PARENT_SCOPE)
|
set(${have_variable} ON PARENT_SCOPE)
|
||||||
set(summary_willbuild "${summary_willbuild};${description}" PARENT_SCOPE)
|
set(summary_willbuild "${summary_willbuild};${description}" PARENT_SCOPE)
|
||||||
92
cmake/Translations.cmake
Normal file
92
cmake/Translations.cmake
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
find_program(GETTEXT_XGETTEXT_EXECUTABLE xgettext REQUIRED)
|
||||||
|
find_program(CAT_EXECUTABLE cat REQUIRED)
|
||||||
|
|
||||||
|
list(APPEND XGETTEXT_OPTIONS
|
||||||
|
--qt
|
||||||
|
--keyword=tr:1,2c
|
||||||
|
--keyword=tr
|
||||||
|
--flag=tr:1:pass-c-format
|
||||||
|
--flag=tr:1:pass-qt-format
|
||||||
|
--keyword=trUtf8
|
||||||
|
--flag=tr:1:pass-c-format
|
||||||
|
--flag=tr:1:pass-qt-format
|
||||||
|
--keyword=translate:2,3c
|
||||||
|
--keyword=translate:2
|
||||||
|
--flag=translate:2:pass-c-format
|
||||||
|
--flag=translate:2:pass-qt-format
|
||||||
|
--keyword=QT_TR_NOOP
|
||||||
|
--flag=QT_TR_NOOP:1:pass-c-format
|
||||||
|
--flag=QT_TR_NOOP:1:pass-qt-format
|
||||||
|
--keyword=QT_TRANSLATE_NOOP:2
|
||||||
|
--flag=QT_TRANSLATE_NOOP:2:pass-c-format
|
||||||
|
--flag=QT_TRANSLATE_NOOP:2:pass-qt-format
|
||||||
|
--keyword=_
|
||||||
|
--flag=_:1:pass-c-format
|
||||||
|
--flag=_:1:pass-qt-format
|
||||||
|
--keyword=N_
|
||||||
|
--flag=N_:1:pass-c-format
|
||||||
|
--flag=N_:1:pass-qt-format
|
||||||
|
--from-code=utf-8
|
||||||
|
)
|
||||||
|
|
||||||
|
execute_process(COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/translations)
|
||||||
|
|
||||||
|
macro(add_pot outfiles header pot)
|
||||||
|
# Make relative filenames for all source files
|
||||||
|
set(add_pot_sources)
|
||||||
|
foreach(_filename ${ARGN})
|
||||||
|
get_filename_component(_absolute_filename ${_filename} ABSOLUTE)
|
||||||
|
file(RELATIVE_PATH _relative_filename ${CMAKE_CURRENT_SOURCE_DIR} ${_absolute_filename})
|
||||||
|
list(APPEND add_pot_sources ${_relative_filename})
|
||||||
|
endforeach(_filename)
|
||||||
|
|
||||||
|
# Generate the .pot
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT ${pot}
|
||||||
|
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||||
|
COMMAND ${GETTEXT_XGETTEXT_EXECUTABLE} ${XGETTEXT_OPTIONS} -C --omit-header --no-location --output="${CMAKE_CURRENT_BINARY_DIR}/pot.temp" ${add_pot_sources}
|
||||||
|
COMMAND cat ${header} ${CMAKE_CURRENT_BINARY_DIR}/pot.temp > ${pot}
|
||||||
|
DEPENDS ${add_pot_sources} ${header}
|
||||||
|
)
|
||||||
|
|
||||||
|
list(APPEND ${outfiles} ${pot})
|
||||||
|
endmacro(add_pot)
|
||||||
|
|
||||||
|
# Syntax is:
|
||||||
|
# add_po(sources_var po_prefix LANGUAGES language1 language2 ... DIRECTORY dir)
|
||||||
|
|
||||||
|
macro(add_po outfiles po_prefix)
|
||||||
|
parse_arguments(ADD_PO
|
||||||
|
"LANGUAGES;DIRECTORY"
|
||||||
|
""
|
||||||
|
${ARGN}
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach (_lang ${ADD_PO_LANGUAGES})
|
||||||
|
set(_po_filename "${_lang}.po")
|
||||||
|
set(_po_filepath "${CMAKE_CURRENT_SOURCE_DIR}/${ADD_PO_DIRECTORY}/${_po_filename}")
|
||||||
|
set(_qm_filename "strawberry_${_lang}.qm")
|
||||||
|
set(_qm_filepath "${CMAKE_CURRENT_BINARY_DIR}/${ADD_PO_DIRECTORY}/${_qm_filename}")
|
||||||
|
|
||||||
|
# Convert the .po files to .qm files
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT ${_qm_filepath}
|
||||||
|
COMMAND ${QT_LCONVERT_EXECUTABLE} ARGS ${_po_filepath} -o ${_qm_filepath} -of qm -target-language ${_lang}
|
||||||
|
DEPENDS ${_po_filepath} ${_po_filepath}
|
||||||
|
)
|
||||||
|
|
||||||
|
list(APPEND ${outfiles} ${_qm_filepath})
|
||||||
|
list(APPEND INSTALL_TRANSLATIONS_FILES ${_qm_filepath})
|
||||||
|
endforeach (_lang)
|
||||||
|
|
||||||
|
# Generate a qrc file for the translations
|
||||||
|
if(NOT INSTALL_TRANSLATIONS)
|
||||||
|
set(_qrc ${CMAKE_CURRENT_BINARY_DIR}/${ADD_PO_DIRECTORY}/translations.qrc)
|
||||||
|
file(WRITE ${_qrc} "<RCC><qresource prefix=\"/${ADD_PO_DIRECTORY}\">")
|
||||||
|
foreach(_lang ${ADD_PO_LANGUAGES})
|
||||||
|
file(APPEND ${_qrc} "<file>${po_prefix}${_lang}.qm</file>")
|
||||||
|
endforeach(_lang)
|
||||||
|
file(APPEND ${_qrc} "</qresource></RCC>")
|
||||||
|
qt_add_resources(${outfiles} ${_qrc})
|
||||||
|
endif()
|
||||||
|
endmacro(add_po)
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
set(STRAWBERRY_VERSION_MAJOR 1)
|
set(STRAWBERRY_VERSION_MAJOR 1)
|
||||||
set(STRAWBERRY_VERSION_MINOR 2)
|
set(STRAWBERRY_VERSION_MINOR 1)
|
||||||
set(STRAWBERRY_VERSION_PATCH 17)
|
set(STRAWBERRY_VERSION_PATCH 3)
|
||||||
#set(STRAWBERRY_VERSION_PRERELEASE rc1)
|
#set(STRAWBERRY_VERSION_PRERELEASE rc1)
|
||||||
|
|
||||||
set(INCLUDE_GIT_REVISION ON)
|
set(INCLUDE_GIT_REVISION ON)
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
tool="${1:-}"
|
|
||||||
shift || true
|
|
||||||
|
|
||||||
if [[ -z "$tool" ]]; then
|
|
||||||
echo "qt_tool_wrapper.sh: missing tool argument" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
base="$(basename "$tool")"
|
|
||||||
|
|
||||||
# Qt LinguistTools (lrelease) prints some noisy informational lines to stderr that
|
|
||||||
# are not actionable during normal builds (e.g. "Removed plural forms...").
|
|
||||||
# We filter only those specific messages.
|
|
||||||
if [[ "$base" == "lrelease" ]]; then
|
|
||||||
"$tool" "$@" 2>&1 | sed \
|
|
||||||
-e '/^Removed plural forms as the target language has less forms\.$/d' \
|
|
||||||
-e '/^If this sounds wrong, possibly the target language is not set or recognized\.$/d'
|
|
||||||
exit "${PIPESTATUS[0]}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
exec "$tool" "$@"
|
|
||||||
|
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
files:
|
files:
|
||||||
- source: /src/translations/strawberry_en_US.ts
|
- source: /src/translations/translations.pot
|
||||||
translation: /src/translations/strawberry_%locale_with_underscore%.ts
|
translation: /src/translations/%locale_with_underscore%.po
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
<file>schema/schema-18.sql</file>
|
<file>schema/schema-18.sql</file>
|
||||||
<file>schema/schema-19.sql</file>
|
<file>schema/schema-19.sql</file>
|
||||||
<file>schema/schema-20.sql</file>
|
<file>schema/schema-20.sql</file>
|
||||||
<file>schema/schema-21.sql</file>
|
|
||||||
<file>schema/device-schema.sql</file>
|
<file>schema/device-schema.sql</file>
|
||||||
<file>style/strawberry.css</file>
|
<file>style/strawberry.css</file>
|
||||||
<file>style/smartplaylistsearchterm.css</file>
|
<file>style/smartplaylistsearchterm.css</file>
|
||||||
|
|||||||
@@ -12,13 +12,9 @@ CREATE TABLE device_%deviceid_subdirectories (
|
|||||||
CREATE TABLE device_%deviceid_songs (
|
CREATE TABLE device_%deviceid_songs (
|
||||||
|
|
||||||
title TEXT,
|
title TEXT,
|
||||||
titlesort TEXT,
|
|
||||||
album TEXT,
|
album TEXT,
|
||||||
albumsort TEXT,
|
|
||||||
artist TEXT,
|
artist TEXT,
|
||||||
artistsort TEXT,
|
|
||||||
albumartist TEXT,
|
albumartist TEXT,
|
||||||
albumartistsort TEXT,
|
|
||||||
track INTEGER NOT NULL DEFAULT -1,
|
track INTEGER NOT NULL DEFAULT -1,
|
||||||
disc INTEGER NOT NULL DEFAULT -1,
|
disc INTEGER NOT NULL DEFAULT -1,
|
||||||
year INTEGER NOT NULL DEFAULT -1,
|
year INTEGER NOT NULL DEFAULT -1,
|
||||||
@@ -26,9 +22,7 @@ CREATE TABLE device_%deviceid_songs (
|
|||||||
genre TEXT,
|
genre TEXT,
|
||||||
compilation INTEGER NOT NULL DEFAULT 0,
|
compilation INTEGER NOT NULL DEFAULT 0,
|
||||||
composer TEXT,
|
composer TEXT,
|
||||||
composersort TEXT,
|
|
||||||
performer TEXT,
|
performer TEXT,
|
||||||
performersort TEXT,
|
|
||||||
grouping TEXT,
|
grouping TEXT,
|
||||||
comment TEXT,
|
comment TEXT,
|
||||||
lyrics TEXT,
|
lyrics TEXT,
|
||||||
@@ -92,11 +86,7 @@ CREATE TABLE device_%deviceid_songs (
|
|||||||
musicbrainz_work_id TEXT,
|
musicbrainz_work_id TEXT,
|
||||||
|
|
||||||
ebur128_integrated_loudness_lufs REAL,
|
ebur128_integrated_loudness_lufs REAL,
|
||||||
ebur128_loudness_range_lu REAL,
|
ebur128_loudness_range_lu REAL
|
||||||
|
|
||||||
bpm REAL,
|
|
||||||
mood TEXT,
|
|
||||||
initial_key TEXT
|
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -104,4 +94,4 @@ CREATE INDEX idx_device_%deviceid_songs_album ON device_%deviceid_songs (album);
|
|||||||
|
|
||||||
CREATE INDEX idx_device_%deviceid_songs_comp_artist ON device_%deviceid_songs (compilation_effective, artist);
|
CREATE INDEX idx_device_%deviceid_songs_comp_artist ON device_%deviceid_songs (compilation_effective, artist);
|
||||||
|
|
||||||
UPDATE devices SET schema_version=6 WHERE ROWID=%deviceid;
|
UPDATE devices SET schema_version=5 WHERE ROWID=%deviceid;
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
DROP INDEX IF EXISTS idx_albumartistsort;
|
|
||||||
|
|
||||||
DROP INDEX IF EXISTS idx_albumsort;
|
|
||||||
|
|
||||||
DROP INDEX IF EXISTS idx_artistsort;
|
|
||||||
|
|
||||||
DROP INDEX IF EXISTS idx_composersort;
|
|
||||||
|
|
||||||
DROP INDEX IF EXISTS idx_performersort;
|
|
||||||
|
|
||||||
DROP INDEX IF EXISTS idx_titlesort;
|
|
||||||
|
|
||||||
ALTER TABLE %allsongstables ADD COLUMN albumartistsort TEXT;
|
|
||||||
|
|
||||||
ALTER TABLE %allsongstables ADD COLUMN albumsort TEXT;
|
|
||||||
|
|
||||||
ALTER TABLE %allsongstables ADD COLUMN artistsort TEXT;
|
|
||||||
|
|
||||||
ALTER TABLE %allsongstables ADD COLUMN composersort TEXT;
|
|
||||||
|
|
||||||
ALTER TABLE %allsongstables ADD COLUMN performersort TEXT;
|
|
||||||
|
|
||||||
ALTER TABLE %allsongstables ADD COLUMN titlesort TEXT;
|
|
||||||
|
|
||||||
ALTER TABLE %allsongstables ADD COLUMN bpm REAL;
|
|
||||||
|
|
||||||
ALTER TABLE %allsongstables ADD COLUMN mood TEXT;
|
|
||||||
|
|
||||||
ALTER TABLE %allsongstables ADD COLUMN initial_key TEXT;
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_albumartistsort ON songs (albumartistsort);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_albumsort ON songs (album);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_artistsort ON songs (artistsort);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_composersort ON songs (title);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_performersort ON songs (title);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_titlesort ON songs (title);
|
|
||||||
|
|
||||||
UPDATE schema_version SET version=21;
|
|
||||||
@@ -4,7 +4,7 @@ CREATE TABLE IF NOT EXISTS schema_version (
|
|||||||
|
|
||||||
DELETE FROM schema_version;
|
DELETE FROM schema_version;
|
||||||
|
|
||||||
INSERT INTO schema_version (version) VALUES (21);
|
INSERT INTO schema_version (version) VALUES (20);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS directories (
|
CREATE TABLE IF NOT EXISTS directories (
|
||||||
path TEXT NOT NULL,
|
path TEXT NOT NULL,
|
||||||
@@ -20,13 +20,9 @@ CREATE TABLE IF NOT EXISTS subdirectories (
|
|||||||
CREATE TABLE IF NOT EXISTS songs (
|
CREATE TABLE IF NOT EXISTS songs (
|
||||||
|
|
||||||
title TEXT,
|
title TEXT,
|
||||||
titlesort TEXT,
|
|
||||||
album TEXT,
|
album TEXT,
|
||||||
albumsort TEXT,
|
|
||||||
artist TEXT,
|
artist TEXT,
|
||||||
artistsort TEXT,
|
|
||||||
albumartist TEXT,
|
albumartist TEXT,
|
||||||
albumartistsort TEXT,
|
|
||||||
track INTEGER NOT NULL DEFAULT -1,
|
track INTEGER NOT NULL DEFAULT -1,
|
||||||
disc INTEGER NOT NULL DEFAULT -1,
|
disc INTEGER NOT NULL DEFAULT -1,
|
||||||
year INTEGER NOT NULL DEFAULT -1,
|
year INTEGER NOT NULL DEFAULT -1,
|
||||||
@@ -34,9 +30,7 @@ CREATE TABLE IF NOT EXISTS songs (
|
|||||||
genre TEXT,
|
genre TEXT,
|
||||||
compilation INTEGER NOT NULL DEFAULT 0,
|
compilation INTEGER NOT NULL DEFAULT 0,
|
||||||
composer TEXT,
|
composer TEXT,
|
||||||
composersort TEXT,
|
|
||||||
performer TEXT,
|
performer TEXT,
|
||||||
performersort TEXT,
|
|
||||||
grouping TEXT,
|
grouping TEXT,
|
||||||
comment TEXT,
|
comment TEXT,
|
||||||
lyrics TEXT,
|
lyrics TEXT,
|
||||||
@@ -100,24 +94,16 @@ CREATE TABLE IF NOT EXISTS songs (
|
|||||||
musicbrainz_work_id TEXT,
|
musicbrainz_work_id TEXT,
|
||||||
|
|
||||||
ebur128_integrated_loudness_lufs REAL,
|
ebur128_integrated_loudness_lufs REAL,
|
||||||
ebur128_loudness_range_lu REAL,
|
ebur128_loudness_range_lu REAL
|
||||||
|
|
||||||
bpm REAL,
|
|
||||||
mood TEXT,
|
|
||||||
initial_key TEXT
|
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS subsonic_songs (
|
CREATE TABLE IF NOT EXISTS subsonic_songs (
|
||||||
|
|
||||||
title TEXT,
|
title TEXT,
|
||||||
titlesort TEXT,
|
|
||||||
album TEXT,
|
album TEXT,
|
||||||
albumsort TEXT,
|
|
||||||
artist TEXT,
|
artist TEXT,
|
||||||
artistsort TEXT,
|
|
||||||
albumartist TEXT,
|
albumartist TEXT,
|
||||||
albumartistsort TEXT,
|
|
||||||
track INTEGER NOT NULL DEFAULT -1,
|
track INTEGER NOT NULL DEFAULT -1,
|
||||||
disc INTEGER NOT NULL DEFAULT -1,
|
disc INTEGER NOT NULL DEFAULT -1,
|
||||||
year INTEGER NOT NULL DEFAULT -1,
|
year INTEGER NOT NULL DEFAULT -1,
|
||||||
@@ -125,9 +111,7 @@ CREATE TABLE IF NOT EXISTS subsonic_songs (
|
|||||||
genre TEXT,
|
genre TEXT,
|
||||||
compilation INTEGER NOT NULL DEFAULT 0,
|
compilation INTEGER NOT NULL DEFAULT 0,
|
||||||
composer TEXT,
|
composer TEXT,
|
||||||
composersort TEXT,
|
|
||||||
performer TEXT,
|
performer TEXT,
|
||||||
performersort TEXT,
|
|
||||||
grouping TEXT,
|
grouping TEXT,
|
||||||
comment TEXT,
|
comment TEXT,
|
||||||
lyrics TEXT,
|
lyrics TEXT,
|
||||||
@@ -191,24 +175,16 @@ CREATE TABLE IF NOT EXISTS subsonic_songs (
|
|||||||
musicbrainz_work_id TEXT,
|
musicbrainz_work_id TEXT,
|
||||||
|
|
||||||
ebur128_integrated_loudness_lufs REAL,
|
ebur128_integrated_loudness_lufs REAL,
|
||||||
ebur128_loudness_range_lu REAL,
|
ebur128_loudness_range_lu REAL
|
||||||
|
|
||||||
bpm REAL,
|
|
||||||
mood TEXT,
|
|
||||||
initial_key TEXT
|
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS tidal_artists_songs (
|
CREATE TABLE IF NOT EXISTS tidal_artists_songs (
|
||||||
|
|
||||||
title TEXT,
|
title TEXT,
|
||||||
titlesort TEXT,
|
|
||||||
album TEXT,
|
album TEXT,
|
||||||
albumsort TEXT,
|
|
||||||
artist TEXT,
|
artist TEXT,
|
||||||
artistsort TEXT,
|
|
||||||
albumartist TEXT,
|
albumartist TEXT,
|
||||||
albumartistsort TEXT,
|
|
||||||
track INTEGER NOT NULL DEFAULT -1,
|
track INTEGER NOT NULL DEFAULT -1,
|
||||||
disc INTEGER NOT NULL DEFAULT -1,
|
disc INTEGER NOT NULL DEFAULT -1,
|
||||||
year INTEGER NOT NULL DEFAULT -1,
|
year INTEGER NOT NULL DEFAULT -1,
|
||||||
@@ -216,9 +192,7 @@ CREATE TABLE IF NOT EXISTS tidal_artists_songs (
|
|||||||
genre TEXT,
|
genre TEXT,
|
||||||
compilation INTEGER NOT NULL DEFAULT 0,
|
compilation INTEGER NOT NULL DEFAULT 0,
|
||||||
composer TEXT,
|
composer TEXT,
|
||||||
composersort TEXT,
|
|
||||||
performer TEXT,
|
performer TEXT,
|
||||||
performersort TEXT,
|
|
||||||
grouping TEXT,
|
grouping TEXT,
|
||||||
comment TEXT,
|
comment TEXT,
|
||||||
lyrics TEXT,
|
lyrics TEXT,
|
||||||
@@ -282,24 +256,16 @@ CREATE TABLE IF NOT EXISTS tidal_artists_songs (
|
|||||||
musicbrainz_work_id TEXT,
|
musicbrainz_work_id TEXT,
|
||||||
|
|
||||||
ebur128_integrated_loudness_lufs REAL,
|
ebur128_integrated_loudness_lufs REAL,
|
||||||
ebur128_loudness_range_lu REAL,
|
ebur128_loudness_range_lu REAL
|
||||||
|
|
||||||
bpm REAL,
|
|
||||||
mood TEXT,
|
|
||||||
initial_key TEXT
|
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS tidal_albums_songs (
|
CREATE TABLE IF NOT EXISTS tidal_albums_songs (
|
||||||
|
|
||||||
title TEXT,
|
title TEXT,
|
||||||
titlesort TEXT,
|
|
||||||
album TEXT,
|
album TEXT,
|
||||||
albumsort TEXT,
|
|
||||||
artist TEXT,
|
artist TEXT,
|
||||||
artistsort TEXT,
|
|
||||||
albumartist TEXT,
|
albumartist TEXT,
|
||||||
albumartistsort TEXT,
|
|
||||||
track INTEGER NOT NULL DEFAULT -1,
|
track INTEGER NOT NULL DEFAULT -1,
|
||||||
disc INTEGER NOT NULL DEFAULT -1,
|
disc INTEGER NOT NULL DEFAULT -1,
|
||||||
year INTEGER NOT NULL DEFAULT -1,
|
year INTEGER NOT NULL DEFAULT -1,
|
||||||
@@ -307,9 +273,7 @@ CREATE TABLE IF NOT EXISTS tidal_albums_songs (
|
|||||||
genre TEXT,
|
genre TEXT,
|
||||||
compilation INTEGER NOT NULL DEFAULT 0,
|
compilation INTEGER NOT NULL DEFAULT 0,
|
||||||
composer TEXT,
|
composer TEXT,
|
||||||
composersort TEXT,
|
|
||||||
performer TEXT,
|
performer TEXT,
|
||||||
performersort TEXT,
|
|
||||||
grouping TEXT,
|
grouping TEXT,
|
||||||
comment TEXT,
|
comment TEXT,
|
||||||
lyrics TEXT,
|
lyrics TEXT,
|
||||||
@@ -373,24 +337,16 @@ CREATE TABLE IF NOT EXISTS tidal_albums_songs (
|
|||||||
musicbrainz_work_id TEXT,
|
musicbrainz_work_id TEXT,
|
||||||
|
|
||||||
ebur128_integrated_loudness_lufs REAL,
|
ebur128_integrated_loudness_lufs REAL,
|
||||||
ebur128_loudness_range_lu REAL,
|
ebur128_loudness_range_lu REAL
|
||||||
|
|
||||||
bpm REAL,
|
|
||||||
mood TEXT,
|
|
||||||
initial_key TEXT
|
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS tidal_songs (
|
CREATE TABLE IF NOT EXISTS tidal_songs (
|
||||||
|
|
||||||
title TEXT,
|
title TEXT,
|
||||||
titlesort TEXT,
|
|
||||||
album TEXT,
|
album TEXT,
|
||||||
albumsort TEXT,
|
|
||||||
artist TEXT,
|
artist TEXT,
|
||||||
artistsort TEXT,
|
|
||||||
albumartist TEXT,
|
albumartist TEXT,
|
||||||
albumartistsort TEXT,
|
|
||||||
track INTEGER NOT NULL DEFAULT -1,
|
track INTEGER NOT NULL DEFAULT -1,
|
||||||
disc INTEGER NOT NULL DEFAULT -1,
|
disc INTEGER NOT NULL DEFAULT -1,
|
||||||
year INTEGER NOT NULL DEFAULT -1,
|
year INTEGER NOT NULL DEFAULT -1,
|
||||||
@@ -398,9 +354,7 @@ CREATE TABLE IF NOT EXISTS tidal_songs (
|
|||||||
genre TEXT,
|
genre TEXT,
|
||||||
compilation INTEGER NOT NULL DEFAULT 0,
|
compilation INTEGER NOT NULL DEFAULT 0,
|
||||||
composer TEXT,
|
composer TEXT,
|
||||||
composersort TEXT,
|
|
||||||
performer TEXT,
|
performer TEXT,
|
||||||
performersort TEXT,
|
|
||||||
grouping TEXT,
|
grouping TEXT,
|
||||||
comment TEXT,
|
comment TEXT,
|
||||||
lyrics TEXT,
|
lyrics TEXT,
|
||||||
@@ -464,24 +418,16 @@ CREATE TABLE IF NOT EXISTS tidal_songs (
|
|||||||
musicbrainz_work_id TEXT,
|
musicbrainz_work_id TEXT,
|
||||||
|
|
||||||
ebur128_integrated_loudness_lufs REAL,
|
ebur128_integrated_loudness_lufs REAL,
|
||||||
ebur128_loudness_range_lu REAL,
|
ebur128_loudness_range_lu REAL
|
||||||
|
|
||||||
bpm REAL,
|
|
||||||
mood TEXT,
|
|
||||||
initial_key TEXT
|
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS spotify_artists_songs (
|
CREATE TABLE IF NOT EXISTS spotify_artists_songs (
|
||||||
|
|
||||||
title TEXT,
|
title TEXT,
|
||||||
titlesort TEXT,
|
|
||||||
album TEXT,
|
album TEXT,
|
||||||
albumsort TEXT,
|
|
||||||
artist TEXT,
|
artist TEXT,
|
||||||
artistsort TEXT,
|
|
||||||
albumartist TEXT,
|
albumartist TEXT,
|
||||||
albumartistsort TEXT,
|
|
||||||
track INTEGER NOT NULL DEFAULT -1,
|
track INTEGER NOT NULL DEFAULT -1,
|
||||||
disc INTEGER NOT NULL DEFAULT -1,
|
disc INTEGER NOT NULL DEFAULT -1,
|
||||||
year INTEGER NOT NULL DEFAULT -1,
|
year INTEGER NOT NULL DEFAULT -1,
|
||||||
@@ -489,9 +435,7 @@ CREATE TABLE IF NOT EXISTS spotify_artists_songs (
|
|||||||
genre TEXT,
|
genre TEXT,
|
||||||
compilation INTEGER NOT NULL DEFAULT 0,
|
compilation INTEGER NOT NULL DEFAULT 0,
|
||||||
composer TEXT,
|
composer TEXT,
|
||||||
composersort TEXT,
|
|
||||||
performer TEXT,
|
performer TEXT,
|
||||||
performersort TEXT,
|
|
||||||
grouping TEXT,
|
grouping TEXT,
|
||||||
comment TEXT,
|
comment TEXT,
|
||||||
lyrics TEXT,
|
lyrics TEXT,
|
||||||
@@ -555,24 +499,16 @@ CREATE TABLE IF NOT EXISTS spotify_artists_songs (
|
|||||||
musicbrainz_work_id TEXT,
|
musicbrainz_work_id TEXT,
|
||||||
|
|
||||||
ebur128_integrated_loudness_lufs REAL,
|
ebur128_integrated_loudness_lufs REAL,
|
||||||
ebur128_loudness_range_lu REAL,
|
ebur128_loudness_range_lu REAL
|
||||||
|
|
||||||
bpm REAL,
|
|
||||||
mood TEXT,
|
|
||||||
initial_key TEXT
|
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS spotify_albums_songs (
|
CREATE TABLE IF NOT EXISTS spotify_albums_songs (
|
||||||
|
|
||||||
title TEXT,
|
title TEXT,
|
||||||
titlesort TEXT,
|
|
||||||
album TEXT,
|
album TEXT,
|
||||||
albumsort TEXT,
|
|
||||||
artist TEXT,
|
artist TEXT,
|
||||||
artistsort TEXT,
|
|
||||||
albumartist TEXT,
|
albumartist TEXT,
|
||||||
albumartistsort TEXT,
|
|
||||||
track INTEGER NOT NULL DEFAULT -1,
|
track INTEGER NOT NULL DEFAULT -1,
|
||||||
disc INTEGER NOT NULL DEFAULT -1,
|
disc INTEGER NOT NULL DEFAULT -1,
|
||||||
year INTEGER NOT NULL DEFAULT -1,
|
year INTEGER NOT NULL DEFAULT -1,
|
||||||
@@ -580,9 +516,7 @@ CREATE TABLE IF NOT EXISTS spotify_albums_songs (
|
|||||||
genre TEXT,
|
genre TEXT,
|
||||||
compilation INTEGER NOT NULL DEFAULT 0,
|
compilation INTEGER NOT NULL DEFAULT 0,
|
||||||
composer TEXT,
|
composer TEXT,
|
||||||
composersort TEXT,
|
|
||||||
performer TEXT,
|
performer TEXT,
|
||||||
performersort TEXT,
|
|
||||||
grouping TEXT,
|
grouping TEXT,
|
||||||
comment TEXT,
|
comment TEXT,
|
||||||
lyrics TEXT,
|
lyrics TEXT,
|
||||||
@@ -646,24 +580,16 @@ CREATE TABLE IF NOT EXISTS spotify_albums_songs (
|
|||||||
musicbrainz_work_id TEXT,
|
musicbrainz_work_id TEXT,
|
||||||
|
|
||||||
ebur128_integrated_loudness_lufs REAL,
|
ebur128_integrated_loudness_lufs REAL,
|
||||||
ebur128_loudness_range_lu REAL,
|
ebur128_loudness_range_lu REAL
|
||||||
|
|
||||||
bpm REAL,
|
|
||||||
mood TEXT,
|
|
||||||
initial_key TEXT
|
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS spotify_songs (
|
CREATE TABLE IF NOT EXISTS spotify_songs (
|
||||||
|
|
||||||
title TEXT,
|
title TEXT,
|
||||||
titlesort TEXT,
|
|
||||||
album TEXT,
|
album TEXT,
|
||||||
albumsort TEXT,
|
|
||||||
artist TEXT,
|
artist TEXT,
|
||||||
artistsort TEXT,
|
|
||||||
albumartist TEXT,
|
albumartist TEXT,
|
||||||
albumartistsort TEXT,
|
|
||||||
track INTEGER NOT NULL DEFAULT -1,
|
track INTEGER NOT NULL DEFAULT -1,
|
||||||
disc INTEGER NOT NULL DEFAULT -1,
|
disc INTEGER NOT NULL DEFAULT -1,
|
||||||
year INTEGER NOT NULL DEFAULT -1,
|
year INTEGER NOT NULL DEFAULT -1,
|
||||||
@@ -671,9 +597,7 @@ CREATE TABLE IF NOT EXISTS spotify_songs (
|
|||||||
genre TEXT,
|
genre TEXT,
|
||||||
compilation INTEGER NOT NULL DEFAULT 0,
|
compilation INTEGER NOT NULL DEFAULT 0,
|
||||||
composer TEXT,
|
composer TEXT,
|
||||||
composersort TEXT,
|
|
||||||
performer TEXT,
|
performer TEXT,
|
||||||
performersort TEXT,
|
|
||||||
grouping TEXT,
|
grouping TEXT,
|
||||||
comment TEXT,
|
comment TEXT,
|
||||||
lyrics TEXT,
|
lyrics TEXT,
|
||||||
@@ -737,24 +661,16 @@ CREATE TABLE IF NOT EXISTS spotify_songs (
|
|||||||
musicbrainz_work_id TEXT,
|
musicbrainz_work_id TEXT,
|
||||||
|
|
||||||
ebur128_integrated_loudness_lufs REAL,
|
ebur128_integrated_loudness_lufs REAL,
|
||||||
ebur128_loudness_range_lu REAL,
|
ebur128_loudness_range_lu REAL
|
||||||
|
|
||||||
bpm REAL,
|
|
||||||
mood TEXT,
|
|
||||||
initial_key TEXT
|
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS qobuz_artists_songs (
|
CREATE TABLE IF NOT EXISTS qobuz_artists_songs (
|
||||||
|
|
||||||
title TEXT,
|
title TEXT,
|
||||||
titlesort TEXT,
|
|
||||||
album TEXT,
|
album TEXT,
|
||||||
albumsort TEXT,
|
|
||||||
artist TEXT,
|
artist TEXT,
|
||||||
artistsort TEXT,
|
|
||||||
albumartist TEXT,
|
albumartist TEXT,
|
||||||
albumartistsort TEXT,
|
|
||||||
track INTEGER NOT NULL DEFAULT -1,
|
track INTEGER NOT NULL DEFAULT -1,
|
||||||
disc INTEGER NOT NULL DEFAULT -1,
|
disc INTEGER NOT NULL DEFAULT -1,
|
||||||
year INTEGER NOT NULL DEFAULT -1,
|
year INTEGER NOT NULL DEFAULT -1,
|
||||||
@@ -762,9 +678,7 @@ CREATE TABLE IF NOT EXISTS qobuz_artists_songs (
|
|||||||
genre TEXT,
|
genre TEXT,
|
||||||
compilation INTEGER NOT NULL DEFAULT 0,
|
compilation INTEGER NOT NULL DEFAULT 0,
|
||||||
composer TEXT,
|
composer TEXT,
|
||||||
composersort TEXT,
|
|
||||||
performer TEXT,
|
performer TEXT,
|
||||||
performersort TEXT,
|
|
||||||
grouping TEXT,
|
grouping TEXT,
|
||||||
comment TEXT,
|
comment TEXT,
|
||||||
lyrics TEXT,
|
lyrics TEXT,
|
||||||
@@ -828,24 +742,16 @@ CREATE TABLE IF NOT EXISTS qobuz_artists_songs (
|
|||||||
musicbrainz_work_id TEXT,
|
musicbrainz_work_id TEXT,
|
||||||
|
|
||||||
ebur128_integrated_loudness_lufs REAL,
|
ebur128_integrated_loudness_lufs REAL,
|
||||||
ebur128_loudness_range_lu REAL,
|
ebur128_loudness_range_lu REAL
|
||||||
|
|
||||||
bpm REAL,
|
|
||||||
mood TEXT,
|
|
||||||
initial_key TEXT
|
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS qobuz_albums_songs (
|
CREATE TABLE IF NOT EXISTS qobuz_albums_songs (
|
||||||
|
|
||||||
title TEXT,
|
title TEXT,
|
||||||
titlesort TEXT,
|
|
||||||
album TEXT,
|
album TEXT,
|
||||||
albumsort TEXT,
|
|
||||||
artist TEXT,
|
artist TEXT,
|
||||||
artistsort TEXT,
|
|
||||||
albumartist TEXT,
|
albumartist TEXT,
|
||||||
albumartistsort TEXT,
|
|
||||||
track INTEGER NOT NULL DEFAULT -1,
|
track INTEGER NOT NULL DEFAULT -1,
|
||||||
disc INTEGER NOT NULL DEFAULT -1,
|
disc INTEGER NOT NULL DEFAULT -1,
|
||||||
year INTEGER NOT NULL DEFAULT -1,
|
year INTEGER NOT NULL DEFAULT -1,
|
||||||
@@ -853,9 +759,7 @@ CREATE TABLE IF NOT EXISTS qobuz_albums_songs (
|
|||||||
genre TEXT,
|
genre TEXT,
|
||||||
compilation INTEGER NOT NULL DEFAULT 0,
|
compilation INTEGER NOT NULL DEFAULT 0,
|
||||||
composer TEXT,
|
composer TEXT,
|
||||||
composersort TEXT,
|
|
||||||
performer TEXT,
|
performer TEXT,
|
||||||
performersort TEXT,
|
|
||||||
grouping TEXT,
|
grouping TEXT,
|
||||||
comment TEXT,
|
comment TEXT,
|
||||||
lyrics TEXT,
|
lyrics TEXT,
|
||||||
@@ -919,24 +823,16 @@ CREATE TABLE IF NOT EXISTS qobuz_albums_songs (
|
|||||||
musicbrainz_work_id TEXT,
|
musicbrainz_work_id TEXT,
|
||||||
|
|
||||||
ebur128_integrated_loudness_lufs REAL,
|
ebur128_integrated_loudness_lufs REAL,
|
||||||
ebur128_loudness_range_lu REAL,
|
ebur128_loudness_range_lu REAL
|
||||||
|
|
||||||
bpm REAL,
|
|
||||||
mood TEXT,
|
|
||||||
initial_key TEXT
|
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS qobuz_songs (
|
CREATE TABLE IF NOT EXISTS qobuz_songs (
|
||||||
|
|
||||||
title TEXT,
|
title TEXT,
|
||||||
titlesort TEXT,
|
|
||||||
album TEXT,
|
album TEXT,
|
||||||
albumsort TEXT,
|
|
||||||
artist TEXT,
|
artist TEXT,
|
||||||
artistsort TEXT,
|
|
||||||
albumartist TEXT,
|
albumartist TEXT,
|
||||||
albumartistsort TEXT,
|
|
||||||
track INTEGER NOT NULL DEFAULT -1,
|
track INTEGER NOT NULL DEFAULT -1,
|
||||||
disc INTEGER NOT NULL DEFAULT -1,
|
disc INTEGER NOT NULL DEFAULT -1,
|
||||||
year INTEGER NOT NULL DEFAULT -1,
|
year INTEGER NOT NULL DEFAULT -1,
|
||||||
@@ -944,9 +840,7 @@ CREATE TABLE IF NOT EXISTS qobuz_songs (
|
|||||||
genre TEXT,
|
genre TEXT,
|
||||||
compilation INTEGER NOT NULL DEFAULT 0,
|
compilation INTEGER NOT NULL DEFAULT 0,
|
||||||
composer TEXT,
|
composer TEXT,
|
||||||
composersort TEXT,
|
|
||||||
performer TEXT,
|
performer TEXT,
|
||||||
performersort TEXT,
|
|
||||||
grouping TEXT,
|
grouping TEXT,
|
||||||
comment TEXT,
|
comment TEXT,
|
||||||
lyrics TEXT,
|
lyrics TEXT,
|
||||||
@@ -1010,11 +904,7 @@ CREATE TABLE IF NOT EXISTS qobuz_songs (
|
|||||||
musicbrainz_work_id TEXT,
|
musicbrainz_work_id TEXT,
|
||||||
|
|
||||||
ebur128_integrated_loudness_lufs REAL,
|
ebur128_integrated_loudness_lufs REAL,
|
||||||
ebur128_loudness_range_lu REAL,
|
ebur128_loudness_range_lu REAL
|
||||||
|
|
||||||
bpm REAL,
|
|
||||||
mood TEXT,
|
|
||||||
initial_key TEXT
|
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1041,13 +931,9 @@ CREATE TABLE IF NOT EXISTS playlist_items (
|
|||||||
playlist_url TEXT,
|
playlist_url TEXT,
|
||||||
|
|
||||||
title TEXT,
|
title TEXT,
|
||||||
titlesort TEXT,
|
|
||||||
album TEXT,
|
album TEXT,
|
||||||
albumsort TEXT,
|
|
||||||
artist TEXT,
|
artist TEXT,
|
||||||
artistsort TEXT,
|
|
||||||
albumartist TEXT,
|
albumartist TEXT,
|
||||||
albumartistsort TEXT,
|
|
||||||
track INTEGER,
|
track INTEGER,
|
||||||
disc INTEGER,
|
disc INTEGER,
|
||||||
year INTEGER,
|
year INTEGER,
|
||||||
@@ -1055,9 +941,7 @@ CREATE TABLE IF NOT EXISTS playlist_items (
|
|||||||
genre TEXT,
|
genre TEXT,
|
||||||
compilation INTEGER DEFAULT 0,
|
compilation INTEGER DEFAULT 0,
|
||||||
composer TEXT,
|
composer TEXT,
|
||||||
composersort TEXT,
|
|
||||||
performer TEXT,
|
performer TEXT,
|
||||||
performersort TEXT,
|
|
||||||
grouping TEXT,
|
grouping TEXT,
|
||||||
comment TEXT,
|
comment TEXT,
|
||||||
lyrics TEXT,
|
lyrics TEXT,
|
||||||
@@ -1121,11 +1005,7 @@ CREATE TABLE IF NOT EXISTS playlist_items (
|
|||||||
musicbrainz_work_id TEXT,
|
musicbrainz_work_id TEXT,
|
||||||
|
|
||||||
ebur128_integrated_loudness_lufs REAL,
|
ebur128_integrated_loudness_lufs REAL,
|
||||||
ebur128_loudness_range_lu REAL,
|
ebur128_loudness_range_lu REAL
|
||||||
|
|
||||||
bpm REAL,
|
|
||||||
mood TEXT,
|
|
||||||
initial_key TEXT
|
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1152,22 +1032,10 @@ CREATE INDEX IF NOT EXISTS idx_comp_artist ON songs (compilation_effective, arti
|
|||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_albumartist ON songs (albumartist);
|
CREATE INDEX IF NOT EXISTS idx_albumartist ON songs (albumartist);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_albumartistsort ON songs (albumartistsort);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_artist ON songs (artist);
|
CREATE INDEX IF NOT EXISTS idx_artist ON songs (artist);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_artistsort ON songs (artistsort);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_album ON songs (album);
|
CREATE INDEX IF NOT EXISTS idx_album ON songs (album);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_albumsort ON songs (album);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_title ON songs (title);
|
CREATE INDEX IF NOT EXISTS idx_title ON songs (title);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_titlesort ON songs (title);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_composersort ON songs (title);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_performersort ON songs (title);
|
|
||||||
|
|
||||||
CREATE VIEW IF NOT EXISTS duplicated_songs as select artist dup_artist, album dup_album, title dup_title from songs as inner_songs where artist != '' and album != '' and title != '' and unavailable = 0 group by artist, album , title having count(*) > 1;
|
CREATE VIEW IF NOT EXISTS duplicated_songs as select artist dup_artist, album dup_album, title dup_title from songs as inner_songs where artist != '' and album != '' and title != '' and unavailable = 0 group by artist, album , title having count(*) > 1;
|
||||||
|
|||||||
12
debian/CMakeLists.txt
vendored
12
debian/CMakeLists.txt
vendored
@@ -4,7 +4,19 @@ if(LSB_RELEASE_EXEC AND DPKG_BUILDPACKAGE)
|
|||||||
execute_process(COMMAND /bin/sh "-c" "${LSB_RELEASE_EXEC} -cs" OUTPUT_VARIABLE DEB_CODENAME OUTPUT_STRIP_TRAILING_WHITESPACE)
|
execute_process(COMMAND /bin/sh "-c" "${LSB_RELEASE_EXEC} -cs" OUTPUT_VARIABLE DEB_CODENAME OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||||
|
|
||||||
if(DEB_CODENAME AND DEB_DATE)
|
if(DEB_CODENAME AND DEB_DATE)
|
||||||
|
|
||||||
|
if(QT_VERSION_MAJOR EQUAL 5)
|
||||||
|
set(DEBIAN_BUILD_DEPENDS_QT_PACKAGES qtbase5-dev,qtbase5-dev-tools,qttools5-dev,qttools5-dev-tools,libqt5x11extras5-dev)
|
||||||
|
set(DEBIAN_DEPENDS_QT_PACKAGES libqt5sql5-sqlite)
|
||||||
|
endif()
|
||||||
|
if(QT_VERSION_MAJOR EQUAL 6)
|
||||||
|
set(DEBIAN_BUILD_DEPENDS_QT_PACKAGES qt6-base-dev,qt6-base-dev-tools,qt6-tools-dev,qt6-tools-dev-tools,qt6-l10n-tools)
|
||||||
|
set(DEBIAN_DEPENDS_QT_PACKAGES libqt6sql6-sqlite,qt6-qpa-plugins)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/control.in ${CMAKE_CURRENT_SOURCE_DIR}/control @ONLY)
|
||||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/changelog.in ${CMAKE_CURRENT_SOURCE_DIR}/changelog)
|
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/changelog.in ${CMAKE_CURRENT_SOURCE_DIR}/changelog)
|
||||||
|
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
3
debian/clean
vendored
3
debian/clean
vendored
@@ -1,3 +0,0 @@
|
|||||||
dist/scripts/maketarball.sh
|
|
||||||
CMakeCache.txt
|
|
||||||
CMakeFiles/
|
|
||||||
1
debian/compat
vendored
Normal file
1
debian/compat
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
11
|
||||||
27
debian/control → debian/control.in
vendored
27
debian/control → debian/control.in
vendored
@@ -2,27 +2,23 @@ Source: strawberry
|
|||||||
Section: sound
|
Section: sound
|
||||||
Priority: optional
|
Priority: optional
|
||||||
Maintainer: Jonas Kvinge <jonas@jkvinge.net>
|
Maintainer: Jonas Kvinge <jonas@jkvinge.net>
|
||||||
Build-Depends: debhelper-compat (= 12),
|
Build-Depends: debhelper (>= 11),
|
||||||
git,
|
git,
|
||||||
make,
|
make,
|
||||||
cmake,
|
cmake,
|
||||||
gcc,
|
gcc,
|
||||||
g++,
|
g++,
|
||||||
|
protobuf-compiler,
|
||||||
libglib2.0-dev,
|
libglib2.0-dev,
|
||||||
|
libdbus-1-dev,
|
||||||
|
libprotobuf-dev,
|
||||||
libboost-dev,
|
libboost-dev,
|
||||||
libsqlite3-dev,
|
libsqlite3-dev,
|
||||||
libasound2-dev,
|
libasound2-dev,
|
||||||
libpulse-dev,
|
libpulse-dev,
|
||||||
libtag1-dev,
|
libtag1-dev,
|
||||||
libicu-dev,
|
libicu-dev,
|
||||||
libxkbcommon-dev,
|
@DEBIAN_BUILD_DEPENDS_QT_PACKAGES@,
|
||||||
qt6-base-dev,
|
|
||||||
qt6-base-private-dev,
|
|
||||||
qt6-base-dev-tools,
|
|
||||||
qt6-tools-dev,
|
|
||||||
qt6-tools-dev-tools,
|
|
||||||
qt6-l10n-tools,
|
|
||||||
libkdsingleapplication-qt6-dev,
|
|
||||||
libgstreamer1.0-dev,
|
libgstreamer1.0-dev,
|
||||||
libgstreamer-plugins-base1.0-dev,
|
libgstreamer-plugins-base1.0-dev,
|
||||||
libcdio-dev,
|
libcdio-dev,
|
||||||
@@ -30,17 +26,14 @@ Build-Depends: debhelper-compat (= 12),
|
|||||||
libmtp-dev,
|
libmtp-dev,
|
||||||
libchromaprint-dev,
|
libchromaprint-dev,
|
||||||
libfftw3-dev,
|
libfftw3-dev,
|
||||||
libebur128-dev,
|
libebur128-dev
|
||||||
libsparsehash-dev,
|
Standards-Version: 4.6.1
|
||||||
rapidjson-dev
|
|
||||||
Standards-Version: 4.7.0
|
|
||||||
|
|
||||||
Package: strawberry
|
Package: strawberry
|
||||||
Architecture: any
|
Architecture: any
|
||||||
Depends: ${shlibs:Depends},
|
Depends: ${shlibs:Depends},
|
||||||
${misc:Depends},
|
${misc:Depends},
|
||||||
libqt6sql6-sqlite,
|
@DEBIAN_DEPENDS_QT_PACKAGES@,
|
||||||
qt6-qpa-plugins,
|
|
||||||
gstreamer1.0-plugins-base,
|
gstreamer1.0-plugins-base,
|
||||||
gstreamer1.0-plugins-good,
|
gstreamer1.0-plugins-good,
|
||||||
gstreamer1.0-alsa,
|
gstreamer1.0-alsa,
|
||||||
@@ -60,11 +53,11 @@ Description: music player and music collection organizer
|
|||||||
- Edit tags on audio files
|
- Edit tags on audio files
|
||||||
- Automatically retrieve tags from MusicBrainz
|
- Automatically retrieve tags from MusicBrainz
|
||||||
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
|
- Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify
|
||||||
- Lyrics from multiple sources
|
- Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com and elyrics.net
|
||||||
- Audio analyzer
|
- Audio analyzer
|
||||||
- Audio equalizer
|
- Audio equalizer
|
||||||
- Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
|
- Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic
|
||||||
- Scrobbler with support for Last.fm and ListenBrainz
|
- Scrobbler with support for Last.fm, Libre.fm and ListenBrainz
|
||||||
- Streaming support for Subsonic-compatible servers
|
- Streaming support for Subsonic-compatible servers
|
||||||
- Unofficial streaming support for Tidal and Qobuz
|
- Unofficial streaming support for Tidal and Qobuz
|
||||||
.
|
.
|
||||||
61
debian/copyright
vendored
61
debian/copyright
vendored
@@ -5,12 +5,14 @@ Source: https://github.com/strawberrymusicplayer/strawberry
|
|||||||
|
|
||||||
Files: *
|
Files: *
|
||||||
Copyright: 2010-2015, David Sansome <me@davidsansome.com>
|
Copyright: 2010-2015, David Sansome <me@davidsansome.com>
|
||||||
2012-2014, 2017-2024 Jonas Kvinge <jonas@jkvinge.net>
|
2012-2014, 2017-2023 Jonas Kvinge <jonas@jkvinge.net>
|
||||||
License: GPL-3+
|
License: GPL-3+
|
||||||
|
|
||||||
Files: src/utilities/timeconstants.h
|
Files: src/utilities/timeconstants.h
|
||||||
src/core/logging.cpp
|
ext/libstrawberry-common/core/logging.cpp
|
||||||
src/core/logging.h
|
ext/libstrawberry-common/core/logging.h
|
||||||
|
ext/libstrawberry-common/core/messagehandler.cpp
|
||||||
|
ext/libstrawberry-common/core/messagehandler.h
|
||||||
Copyright: 2011, 2012, David Sansome <me@davidsansome.com>
|
Copyright: 2011, 2012, David Sansome <me@davidsansome.com>
|
||||||
2018-2022, Jonas Kvinge <jonas@jkvinge.net>
|
2018-2022, Jonas Kvinge <jonas@jkvinge.net>
|
||||||
License: Apache-2.0
|
License: Apache-2.0
|
||||||
@@ -29,20 +31,18 @@ Files: src/core/main.h
|
|||||||
src/engine/alsapcmdevicefinder.h
|
src/engine/alsapcmdevicefinder.h
|
||||||
src/engine/mmdevicefinder.cpp
|
src/engine/mmdevicefinder.cpp
|
||||||
src/engine/mmdevicefinder.h
|
src/engine/mmdevicefinder.h
|
||||||
src/engine/uwpdevicefinder.cpp
|
|
||||||
src/engine/uwpdevicefinder.h
|
|
||||||
src/engine/devicefinder.cpp
|
src/engine/devicefinder.cpp
|
||||||
src/engine/devicefinder.h
|
src/engine/devicefinder.h
|
||||||
src/engine/enginedevice.cpp
|
src/engine/enginedevice.cpp
|
||||||
src/engine/enginedevice.h
|
src/engine/enginedevice.h
|
||||||
src/engine/enginemetadata.cpp
|
src/engine/enginemetadata.cpp
|
||||||
src/engine/enginemetadata.h
|
src/engine/enginemetadata.h
|
||||||
src/streaming/streamingservice.cpp
|
src/internet/internetservice.cpp
|
||||||
src/streaming/streamingservice.h
|
src/internet/internetservice.h
|
||||||
src/streaming/streamingtabsview.cpp
|
src/internet/internettabsview.cpp
|
||||||
src/streaming/streamingtabsview.h
|
src/internet/internettabsview.h
|
||||||
src/streaming/streamingsongsview.cpp
|
src/internet/internetsongsview.cpp
|
||||||
src/streaming/streamingsongsview.h
|
src/internet/internetsongsview.h
|
||||||
src/settings/backendsettingspage.cpp
|
src/settings/backendsettingspage.cpp
|
||||||
src/settings/backendsettingspage.h
|
src/settings/backendsettingspage.h
|
||||||
src/settings/coverssettingspage.cpp
|
src/settings/coverssettingspage.cpp
|
||||||
@@ -55,8 +55,6 @@ Files: src/core/main.h
|
|||||||
src/settings/subsonicsettingspage.h
|
src/settings/subsonicsettingspage.h
|
||||||
src/settings/tidalsettingspage.cpp
|
src/settings/tidalsettingspage.cpp
|
||||||
src/settings/tidalsettingspage.h
|
src/settings/tidalsettingspage.h
|
||||||
src/settings/spotifysettingspage.cpp
|
|
||||||
src/settings/spotifysettingspage.h
|
|
||||||
src/covermanager/jsoncoverprovider.cpp
|
src/covermanager/jsoncoverprovider.cpp
|
||||||
src/covermanager/jsoncoverprovider.h
|
src/covermanager/jsoncoverprovider.h
|
||||||
src/covermanager/lastfmcoverprovider.cpp
|
src/covermanager/lastfmcoverprovider.cpp
|
||||||
@@ -67,16 +65,16 @@ Files: src/core/main.h
|
|||||||
src/covermanager/deezercoverprovider.h
|
src/covermanager/deezercoverprovider.h
|
||||||
src/covermanager/tidalcoverprovider.cpp
|
src/covermanager/tidalcoverprovider.cpp
|
||||||
src/covermanager/tidalcoverprovider.h
|
src/covermanager/tidalcoverprovider.h
|
||||||
src/covermanager/opentidalcoverprovider.cpp
|
|
||||||
src/covermanager/opentidalcoverprovider.h
|
|
||||||
src/covermanager/qobuzcoverprovider.cpp
|
src/covermanager/qobuzcoverprovider.cpp
|
||||||
src/covermanager/qobuzcoverprovider.h
|
src/covermanager/qobuzcoverprovider.h
|
||||||
src/covermanager/spotifycoverprovider.cpp
|
src/covermanager/spotifycoverprovider.cpp
|
||||||
src/covermanager/spotifycoverprovider.h
|
src/covermanager/spotifycoverprovider.h
|
||||||
src/covermanager/musixmatchcoverprovider.cpp
|
src/covermanager/musixmatchcoverprovider.cpp
|
||||||
src/covermanager/musixmatchcoverprovider.h
|
src/covermanager/musixmatchcoverprovider.h
|
||||||
src/globalshortcuts/globalshortcutsbackend-kglobalaccel.cpp
|
src/globalshortcuts/globalshortcutsbackend-kde.cpp
|
||||||
src/globalshortcuts/globalshortcutsbackend-kglobalaccel.h
|
src/globalshortcuts/globalshortcutsbackend-kde.h
|
||||||
|
src/globalshortcuts/globalshortcutsbackend-mate.cpp
|
||||||
|
src/globalshortcuts/globalshortcutsbackend-mate.h
|
||||||
src/globalshortcuts/globalshortcutsbackend-x11.cpp
|
src/globalshortcuts/globalshortcutsbackend-x11.cpp
|
||||||
src/globalshortcuts/globalshortcutsbackend-x11.h
|
src/globalshortcuts/globalshortcutsbackend-x11.h
|
||||||
src/globalshortcuts/globalshortcutsbackend-win.cpp
|
src/globalshortcuts/globalshortcutsbackend-win.cpp
|
||||||
@@ -93,14 +91,14 @@ Files: src/core/main.h
|
|||||||
src/tidal/*
|
src/tidal/*
|
||||||
src/qobuz/*
|
src/qobuz/*
|
||||||
src/radios/*
|
src/radios/*
|
||||||
src/spotify/*
|
|
||||||
src/transcoder/transcoderoptionswavpack.cpp
|
src/transcoder/transcoderoptionswavpack.cpp
|
||||||
src/transcoder/transcoderoptionswavpack.h
|
src/transcoder/transcoderoptionswavpack.h
|
||||||
|
ext/libstrawberry-tagreader/tagreadertagparser.cpp
|
||||||
|
ext/libstrawberry-tagreader/tagreadertagparser.h
|
||||||
|
ext/macdeploycheck/*
|
||||||
src/widgets/resizabletextedit.cpp
|
src/widgets/resizabletextedit.cpp
|
||||||
src/widgets/resizabletextedit.h
|
src/widgets/resizabletextedit.h
|
||||||
src/widgets/fancytabdata.cpp
|
Copyright: 2012-2014, 2017-2023, Jonas Kvinge <jonas@jkvinge.net>
|
||||||
src/widgets/fancytabdata.h
|
|
||||||
Copyright: 2012-2014, 2017-2024, Jonas Kvinge <jonas@jkvinge.net>
|
|
||||||
License: GPL-3+
|
License: GPL-3+
|
||||||
|
|
||||||
Files: src/engine/enginebase.cpp
|
Files: src/engine/enginebase.cpp
|
||||||
@@ -121,8 +119,6 @@ License: GPL-2+
|
|||||||
|
|
||||||
Files: src/widgets/fancytabwidget.cpp
|
Files: src/widgets/fancytabwidget.cpp
|
||||||
src/widgets/fancytabwidget.h
|
src/widgets/fancytabwidget.h
|
||||||
src/widgets/fancytabbar.cpp
|
|
||||||
src/widgets/fancytabbar.h
|
|
||||||
Copyright: 2018, Vikram Ambrose <ambroseworks@gmail.com>
|
Copyright: 2018, Vikram Ambrose <ambroseworks@gmail.com>
|
||||||
2018, Jonas Kvinge <jonas@jkvinge.net>
|
2018, Jonas Kvinge <jonas@jkvinge.net>
|
||||||
License: GPL-3+
|
License: GPL-3+
|
||||||
@@ -212,8 +208,8 @@ Files: src/device/udisks2lister.cpp
|
|||||||
Copyright: 2016, Valeriy Malov <jazzvoid@gmail.com>
|
Copyright: 2016, Valeriy Malov <jazzvoid@gmail.com>
|
||||||
License: GPL-3+
|
License: GPL-3+
|
||||||
|
|
||||||
Files: src/core/localredirectserver.cpp
|
Files: src/internet/localredirectserver.cpp
|
||||||
src/core/localredirectserver.h
|
src/internet/localredirectserver.h
|
||||||
Copyright: 2012, 2014, John Maguire <john.maguire@gmail.com>
|
Copyright: 2012, 2014, John Maguire <john.maguire@gmail.com>
|
||||||
2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
2014, Krzysztof Sobiecki <sobkas@gmail.com>
|
||||||
2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
2018-2021, Jonas Kvinge <jonas@jkvinge.net>
|
||||||
@@ -252,24 +248,25 @@ Files: src/core/stylehelper.cpp
|
|||||||
Copyright: 2016 The Qt Company Ltd.
|
Copyright: 2016 The Qt Company Ltd.
|
||||||
License: GPL-3+
|
License: GPL-3+
|
||||||
|
|
||||||
Files: 3rdparty/gstfastspectrum/gstfastspectrum.cpp
|
Files: ext/gstmoodbar/gstfastspectrum.cpp
|
||||||
3rdparty/gstfastspectrum/gstfastspectrum.h
|
ext/gstmoodbar/gstfastspectrum.h
|
||||||
Copyright: 1999 Erik Walthinsen <omega@cse.ogi.edu>
|
Copyright: 1999 Erik Walthinsen <omega@cse.ogi.edu>
|
||||||
2006,2011 Stefan Kost <ensonic@users.sf.net>
|
2006,2011 Stefan Kost <ensonic@users.sf.net>
|
||||||
2007-2009 Sebastian Dröge <sebastian.droege@collabora.co.uk>
|
2007-2009 Sebastian Dröge <sebastian.droege@collabora.co.uk>
|
||||||
2018-2024 Jonas Kvinge <jonas@jkvinge.net>
|
|
||||||
License: GPL-2+
|
License: GPL-2+
|
||||||
|
|
||||||
Files: src/widgets/qsearchfield_qt.cpp
|
Files: src/widgets/qsearchfield_nonmac.cpp
|
||||||
src/widgets/qsearchfield_mac.mm
|
src/widgets/qsearchfield_mac.mm
|
||||||
src/widgets/qsearchfield.h
|
src/widgets/qsearchfield.h
|
||||||
src/widgets/qocoa_mac.h
|
src/widgets/qocoa_mac.h
|
||||||
src/widgets/searchfield_qt_private.cpp
|
|
||||||
src/widgets/searchfield_qt_private.h
|
|
||||||
Copyright: 2011, Mike McQuaid <mike@mikemcquaid.com>
|
Copyright: 2011, Mike McQuaid <mike@mikemcquaid.com>
|
||||||
2018-2024, Jonas Kvinge <jonas@jkvinge.net>
|
|
||||||
License: Expat
|
License: Expat
|
||||||
|
|
||||||
|
Files: 3rdparty/SPMediaKeyTap/*
|
||||||
|
Copyright: 2010, Spotify AB
|
||||||
|
2011, Joachim Bengtsson
|
||||||
|
License: BSD-3-clause
|
||||||
|
|
||||||
Files: 3rdparty/kdsingleapplication/*
|
Files: 3rdparty/kdsingleapplication/*
|
||||||
Copyright: 2019-2023 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
|
Copyright: 2019-2023 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
|
||||||
License: MIT
|
License: MIT
|
||||||
|
|||||||
17
debian/rules
vendored
17
debian/rules
vendored
@@ -1,14 +1,17 @@
|
|||||||
#!/usr/bin/make -f
|
#!/usr/bin/make -f
|
||||||
|
|
||||||
export DH_VERBOSE=1
|
%:
|
||||||
export DEB_BUILD_MAINT_OPTIONS=hardening=+all
|
dh $@ --buildsystem=cmake -builddirectory=build
|
||||||
|
|
||||||
override_dh_auto_configure:
|
override_dh_auto_clean:
|
||||||
dh_auto_configure -- \
|
rm -f dist/macos/Info.plist
|
||||||
-DBUILD_WERROR=ON
|
rm -f dist/unix/strawberry.spec
|
||||||
|
rm -f dist/scripts/maketarball.sh
|
||||||
|
rm -f dist/windows/strawberry.nsi
|
||||||
|
rm -f src/translations/translations.pot
|
||||||
|
dh_auto_clean
|
||||||
|
|
||||||
override_dh_installchangelogs:
|
override_dh_installchangelogs:
|
||||||
dh_installchangelogs Changelog
|
dh_installchangelogs Changelog
|
||||||
|
|
||||||
%:
|
override_dh_auto_test:
|
||||||
dh $@
|
|
||||||
|
|||||||
39
dist/CMakeLists.txt
vendored
39
dist/CMakeLists.txt
vendored
@@ -1,7 +1,7 @@
|
|||||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/scripts/maketarball.sh.in ${CMAKE_CURRENT_SOURCE_DIR}/scripts/maketarball.sh @ONLY)
|
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/scripts/maketarball.sh.in ${CMAKE_CURRENT_SOURCE_DIR}/scripts/maketarball.sh @ONLY)
|
||||||
if(RPM_DISTRO AND RPM_DATE)
|
if(RPM_DISTRO AND RPM_DATE)
|
||||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/unix/strawberry.spec.in ${CMAKE_BINARY_DIR}/strawberry.spec @ONLY)
|
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/unix/strawberry.spec.in ${CMAKE_CURRENT_SOURCE_DIR}/unix/strawberry.spec @ONLY)
|
||||||
endif()
|
endif(RPM_DISTRO AND RPM_DATE)
|
||||||
|
|
||||||
if(APPLE)
|
if(APPLE)
|
||||||
if(DEFINED ENV{MACOSX_DEPLOYMENT_TARGET})
|
if(DEFINED ENV{MACOSX_DEPLOYMENT_TARGET})
|
||||||
@@ -9,33 +9,12 @@ if(APPLE)
|
|||||||
else()
|
else()
|
||||||
set(LSMinimumSystemVersion 12.0)
|
set(LSMinimumSystemVersion 12.0)
|
||||||
endif()
|
endif()
|
||||||
|
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/macos/Info.plist.in ${CMAKE_CURRENT_SOURCE_DIR}/macos/Info.plist)
|
||||||
if(BUILD_FOR_MAC_APP_STORE)
|
endif(APPLE)
|
||||||
# MAS builds must not embed Sparkle update configuration in Info.plist.
|
|
||||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/macos/Info.mas.plist.in ${CMAKE_CURRENT_BINARY_DIR}/macos/Info.plist)
|
|
||||||
else()
|
|
||||||
# Sparkle (macOS updates)
|
|
||||||
# These values are embedded into Info.plist and control where the app checks for updates.
|
|
||||||
# Downstream builders can override on the CMake command line:
|
|
||||||
# -DSPARKLE_FEED_URL="https://example.com/appcast.xml"
|
|
||||||
# -DSPARKLE_PUBLIC_ED25519_KEY="base64=="
|
|
||||||
#
|
|
||||||
# Defaults preserve upstream behavior, but are intentionally configurable for third-party builds.
|
|
||||||
if(NOT DEFINED SPARKLE_FEED_URL)
|
|
||||||
set(SPARKLE_FEED_URL "https://www.strawberrymusicplayer.org/sparkle-macos-@ARCH@")
|
|
||||||
endif()
|
|
||||||
if(NOT DEFINED SPARKLE_PUBLIC_ED25519_KEY)
|
|
||||||
set(SPARKLE_PUBLIC_ED25519_KEY "/OydhYVfypuO2Mf7G6DUqVZWW9G19eFV74qaDCBTOUk=")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/macos/Info.plist.in ${CMAKE_CURRENT_BINARY_DIR}/macos/Info.plist)
|
|
||||||
endif()
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/windows/windres.rc.in ${CMAKE_BINARY_DIR}/windres.rc)
|
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/windows/strawberry.nsi.in ${CMAKE_CURRENT_SOURCE_DIR}/windows/strawberry.nsi @ONLY)
|
||||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/windows/strawberry.nsi.in ${CMAKE_BINARY_DIR}/strawberry.nsi @ONLY)
|
endif(WIN32)
|
||||||
endif()
|
|
||||||
|
|
||||||
if(UNIX AND NOT APPLE)
|
if(UNIX AND NOT APPLE)
|
||||||
install(FILES ../data/icons/48x48/strawberry.png DESTINATION share/icons/hicolor/48x48/apps/)
|
install(FILES ../data/icons/48x48/strawberry.png DESTINATION share/icons/hicolor/48x48/apps/)
|
||||||
@@ -43,10 +22,10 @@ if(UNIX AND NOT APPLE)
|
|||||||
install(FILES ../data/icons/128x128/strawberry.png DESTINATION share/icons/hicolor/128x128/apps/)
|
install(FILES ../data/icons/128x128/strawberry.png DESTINATION share/icons/hicolor/128x128/apps/)
|
||||||
install(FILES unix/org.strawberrymusicplayer.strawberry.desktop DESTINATION share/applications)
|
install(FILES unix/org.strawberrymusicplayer.strawberry.desktop DESTINATION share/applications)
|
||||||
install(FILES unix/org.strawberrymusicplayer.strawberry.appdata.xml DESTINATION share/metainfo)
|
install(FILES unix/org.strawberrymusicplayer.strawberry.appdata.xml DESTINATION share/metainfo)
|
||||||
install(FILES unix/strawberry.1 DESTINATION share/man/man1)
|
install(FILES unix/strawberry.1 unix/strawberry-tagreader.1 DESTINATION share/man/man1)
|
||||||
endif()
|
endif(UNIX AND NOT APPLE)
|
||||||
|
|
||||||
if(APPLE)
|
if(APPLE)
|
||||||
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/macos/Info.plist" DESTINATION "${CMAKE_BINARY_DIR}/strawberry.app/Contents")
|
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/macos/Info.plist" DESTINATION "${CMAKE_BINARY_DIR}/strawberry.app/Contents")
|
||||||
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/macos/strawberry.icns" DESTINATION "${CMAKE_BINARY_DIR}/strawberry.app/Contents/Resources")
|
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/macos/strawberry.icns" DESTINATION "${CMAKE_BINARY_DIR}/strawberry.app/Contents/Resources")
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
253
dist/macos/Info.mas.plist.in
vendored
253
dist/macos/Info.mas.plist.in
vendored
@@ -1,253 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>NSPrincipalClass</key>
|
|
||||||
<string>NSApplication</string>
|
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
|
||||||
<string>English</string>
|
|
||||||
<key>CFBundleExecutable</key>
|
|
||||||
<string>strawberry</string>
|
|
||||||
<key>CFBundleGetInfoString</key>
|
|
||||||
<string>Strawberry ${STRAWBERRY_VERSION_DISPLAY}</string>
|
|
||||||
<key>CFBundleIconFile</key>
|
|
||||||
<string>strawberry.icns</string>
|
|
||||||
<key>CFBundleIdentifier</key>
|
|
||||||
<string>@MACOS_BUNDLE_ID@</string>
|
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
|
||||||
<string>6.0</string>
|
|
||||||
<key>CFBundleLongVersionString</key>
|
|
||||||
<string>${STRAWBERRY_VERSION_DISPLAY}</string>
|
|
||||||
<key>CFBundleName</key>
|
|
||||||
<string>Strawberry</string>
|
|
||||||
<key>CFBundlePackageType</key>
|
|
||||||
<string>APPL</string>
|
|
||||||
<key>CFBundleShortVersionString</key>
|
|
||||||
<string>${STRAWBERRY_VERSION_DISPLAY}</string>
|
|
||||||
<key>CFBundleVersion</key>
|
|
||||||
<string>${STRAWBERRY_VERSION_PACKAGE}</string>
|
|
||||||
<key>CSResourcesFileMapped</key>
|
|
||||||
<true/>
|
|
||||||
<key>LSRequiresCarbon</key>
|
|
||||||
<true/>
|
|
||||||
<key>LSApplicationCategoryType</key>
|
|
||||||
<string>public.app-category.music</string>
|
|
||||||
<key>LSMinimumSystemVersion</key>
|
|
||||||
<string>@LSMinimumSystemVersion@</string>
|
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
|
||||||
<false/>
|
|
||||||
|
|
||||||
<key>CFBundleURLTypes</key>
|
|
||||||
<array>
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleTypeRole</key>
|
|
||||||
<string>Viewer</string>
|
|
||||||
<key>CFBundleURLName</key>
|
|
||||||
<string>@MACOS_BUNDLE_ID@</string>
|
|
||||||
<key>CFBundleURLSchemes</key>
|
|
||||||
<array>
|
|
||||||
<string>tidal</string>
|
|
||||||
</array>
|
|
||||||
</dict>
|
|
||||||
</array>
|
|
||||||
|
|
||||||
<key>CFBundleDocumentTypes</key>
|
|
||||||
<array>
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleTypeOSTypes</key>
|
|
||||||
<array>
|
|
||||||
<string>****</string>
|
|
||||||
<string>fold</string>
|
|
||||||
<string>disk</string>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleTypeRole</key>
|
|
||||||
<string>Viewer</string>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleTypeExtensions</key>
|
|
||||||
<array>
|
|
||||||
<string>xspf</string>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleTypeIconFile</key>
|
|
||||||
<string>Generic.icns</string>
|
|
||||||
<key>CFBundleTypeMIMETypes</key>
|
|
||||||
<array>
|
|
||||||
<string>application/xspf+xml</string>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleTypeName</key>
|
|
||||||
<string>XSPF Playlist</string>
|
|
||||||
<key>CFBundleTypeRole</key>
|
|
||||||
<string>Viewer</string>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleTypeExtensions</key>
|
|
||||||
<array>
|
|
||||||
<string>wav</string>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleTypeMIMETypes</key>
|
|
||||||
<array>
|
|
||||||
<string>audio/x-wav</string>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleTypeName</key>
|
|
||||||
<string>WAVE Audio File</string>
|
|
||||||
<key>CFBundleTypeRole</key>
|
|
||||||
<string>Viewer</string>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleTypeExtensions</key>
|
|
||||||
<array>
|
|
||||||
<string>pls</string>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleTypeIconFile</key>
|
|
||||||
<string>pls.icns</string>
|
|
||||||
<key>CFBundleTypeName</key>
|
|
||||||
<string>Shoutcast playlist</string>
|
|
||||||
<key>CFBundleTypeRole</key>
|
|
||||||
<string>Viewer</string>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleTypeExtensions</key>
|
|
||||||
<array>
|
|
||||||
<string>m3u</string>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleTypeIconFile</key>
|
|
||||||
<string>m3u.icns</string>
|
|
||||||
<key>CFBundleTypeMIMETypes</key>
|
|
||||||
<array>
|
|
||||||
<string>audio/x-mpegurl</string>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleTypeName</key>
|
|
||||||
<string>Playlist file</string>
|
|
||||||
<key>CFBundleTypeRole</key>
|
|
||||||
<string>Viewer</string>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleTypeExtensions</key>
|
|
||||||
<array>
|
|
||||||
<string>aac</string>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleTypeIconFile</key>
|
|
||||||
<string>mpeg4.icns</string>
|
|
||||||
<key>CFBundleTypeName</key>
|
|
||||||
<string>AAC file</string>
|
|
||||||
<key>CFBundleTypeRole</key>
|
|
||||||
<string>Viewer</string>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleTypeExtensions</key>
|
|
||||||
<array>
|
|
||||||
<string>ogg</string>
|
|
||||||
<string>ogx</string>
|
|
||||||
<string>ogm</string>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleTypeIconFile</key>
|
|
||||||
<string>ogg.icns</string>
|
|
||||||
<key>CFBundleTypeMIMETypes</key>
|
|
||||||
<array>
|
|
||||||
<string>audio/ogg</string>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleTypeName</key>
|
|
||||||
<string>Ogg Vorbis File</string>
|
|
||||||
<key>CFBundleTypeRole</key>
|
|
||||||
<string>Viewer</string>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleTypeExtensions</key>
|
|
||||||
<array>
|
|
||||||
<string>oga</string>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleTypeIconFile</key>
|
|
||||||
<string>ogg.icns</string>
|
|
||||||
<key>CFBundleTypeMIMETypes</key>
|
|
||||||
<array>
|
|
||||||
<string>audio/ogg</string>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleTypeName</key>
|
|
||||||
<string>Ogg Audio File</string>
|
|
||||||
<key>CFBundleTypeRole</key>
|
|
||||||
<string>Viewer</string>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleTypeExtensions</key>
|
|
||||||
<array>
|
|
||||||
<string>wma</string>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleTypeIconFile</key>
|
|
||||||
<string>wma.icns</string>
|
|
||||||
<key>CFBundleTypeName</key>
|
|
||||||
<string>WIndows Media Audio</string>
|
|
||||||
<key>CFBundleTypeRole</key>
|
|
||||||
<string>Viewer</string>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleTypeExtensions</key>
|
|
||||||
<array>
|
|
||||||
<string>mp3</string>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleTypeIconFile</key>
|
|
||||||
<string>mp3.icns</string>
|
|
||||||
<key>CFBundleTypeMIMETypes</key>
|
|
||||||
<array>
|
|
||||||
<string>audio/mpeg</string>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleTypeName</key>
|
|
||||||
<string>MPEG Audio Layer 3</string>
|
|
||||||
<key>CFBundleTypeRole</key>
|
|
||||||
<string>Viewer</string>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleTypeExtensions</key>
|
|
||||||
<array>
|
|
||||||
<string>3gp</string>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleTypeIconFile</key>
|
|
||||||
<string>generic.icns</string>
|
|
||||||
<key>CFBundleTypeName</key>
|
|
||||||
<string>3GPP File</string>
|
|
||||||
<key>CFBundleTypeRole</key>
|
|
||||||
<string>Viewer</string>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleTypeExtensions</key>
|
|
||||||
<array>
|
|
||||||
<string>m4a</string>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleTypeIconFile</key>
|
|
||||||
<string>mpeg4.icns</string>
|
|
||||||
<key>CFBundleTypeName</key>
|
|
||||||
<string>MPEG-4 Audio File</string>
|
|
||||||
<key>CFBundleTypeRole</key>
|
|
||||||
<string>Viewer</string>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleTypeExtensions</key>
|
|
||||||
<array>
|
|
||||||
<string>mpc</string>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleTypeIconFile</key>
|
|
||||||
<string>generic.icns</string>
|
|
||||||
<key>CFBundleTypeName</key>
|
|
||||||
<string>Musepack Audio File</string>
|
|
||||||
<key>CFBundleTypeRole</key>
|
|
||||||
<string>Viewer</string>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleTypeExtensions</key>
|
|
||||||
<array>
|
|
||||||
<string>flac</string>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleTypeIconFile</key>
|
|
||||||
<string>generic.icns</string>
|
|
||||||
<key>CFBundleTypeMIMETypes</key>
|
|
||||||
<array>
|
|
||||||
<string>audio/flac</string>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleTypeName</key>
|
|
||||||
<string>FLAC Audio File</string>
|
|
||||||
<key>CFBundleTypeRole</key>
|
|
||||||
<string>Viewer</string>
|
|
||||||
</dict>
|
|
||||||
</array>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
|
|
||||||
15
dist/macos/Info.plist.in
vendored
15
dist/macos/Info.plist.in
vendored
@@ -13,7 +13,7 @@
|
|||||||
<key>CFBundleIconFile</key>
|
<key>CFBundleIconFile</key>
|
||||||
<string>strawberry.icns</string>
|
<string>strawberry.icns</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>@MACOS_BUNDLE_ID@</string>
|
<string>org.strawberrymusicplayer.strawberry</string>
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleLongVersionString</key>
|
<key>CFBundleLongVersionString</key>
|
||||||
@@ -34,24 +34,17 @@
|
|||||||
<string>public.app-category.music</string>
|
<string>public.app-category.music</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
<string>@LSMinimumSystemVersion@</string>
|
<string>@LSMinimumSystemVersion@</string>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
|
||||||
<false/>
|
|
||||||
<!-- Default to manual update checks unless the user explicitly enables automatic checking. -->
|
|
||||||
<key>SUEnableAutomaticChecks</key>
|
|
||||||
<false/>
|
|
||||||
<key>SUAutomaticallyUpdate</key>
|
|
||||||
<false/>
|
|
||||||
<key>SUFeedURL</key>
|
<key>SUFeedURL</key>
|
||||||
<string>@SPARKLE_FEED_URL@</string>
|
<string>https://www.strawberrymusicplayer.org/sparkle-macos</string>
|
||||||
<key>SUPublicEDKey</key>
|
<key>SUPublicEDKey</key>
|
||||||
<string>@SPARKLE_PUBLIC_ED25519_KEY@</string>
|
<string>3IRScV8YtNVnx7zoeJAXvg28Kh1gN/Pyl2iPM467pG8=</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
<array>
|
<array>
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleTypeRole</key>
|
<key>CFBundleTypeRole</key>
|
||||||
<string>Viewer</string>
|
<string>Viewer</string>
|
||||||
<key>CFBundleURLName</key>
|
<key>CFBundleURLName</key>
|
||||||
<string>@MACOS_BUNDLE_ID@</string>
|
<string>org.strawberrymusicplayer.strawberry</string>
|
||||||
<key>CFBundleURLSchemes</key>
|
<key>CFBundleURLSchemes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>tidal</string>
|
<string>tidal</string>
|
||||||
|
|||||||
85
dist/macos/bundle_sparkle.sh
vendored
85
dist/macos/bundle_sparkle.sh
vendored
@@ -1,85 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
bundledir="${1:-}"
|
|
||||||
sparkle_framework_dir="${2:-}"
|
|
||||||
sparkle_bin_link="${3:-}"
|
|
||||||
sparkle_bin_real="${4:-}"
|
|
||||||
|
|
||||||
if [[ -z "$bundledir" || -z "$sparkle_framework_dir" ]]; then
|
|
||||||
echo "Usage: $0 <bundledir> <sparkle_framework_dir> [sparkle_bin_link] [sparkle_bin_real]" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ ! -d "$sparkle_framework_dir" ]]; then
|
|
||||||
echo "Sparkle.framework dir not found: $sparkle_framework_dir" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
src_framework_dir="$sparkle_framework_dir"
|
|
||||||
|
|
||||||
# Homebrew often provides /opt/homebrew/Frameworks/Sparkle.framework where Versions/* are symlinks
|
|
||||||
# pointing back into the Cellar. Copying that verbatim breaks inside an app bundle.
|
|
||||||
# Resolve to the real Cellar framework root via Versions/Current.
|
|
||||||
if [[ -e "${sparkle_framework_dir}/Versions/Current" ]]; then
|
|
||||||
current_real="$(cd "${sparkle_framework_dir}/Versions/Current" && pwd -P)"
|
|
||||||
# current_real is .../Sparkle.framework/Versions/B (or similar)
|
|
||||||
src_framework_dir="$(cd "${current_real}/../.." && pwd -P)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
dst_framework="${bundledir}/Contents/Frameworks/Sparkle.framework"
|
|
||||||
main_bin="${bundledir}/Contents/MacOS/strawberry"
|
|
||||||
qtsparkle_dylib="${bundledir}/Contents/Frameworks/libqtsparkle-qt6.dylib"
|
|
||||||
|
|
||||||
mkdir -p "${bundledir}/Contents/Frameworks"
|
|
||||||
|
|
||||||
echo "Bundling Sparkle.framework -> ${dst_framework}"
|
|
||||||
rm -rf "${dst_framework}"
|
|
||||||
# Use ditto to preserve the framework's internal symlinks/structure.
|
|
||||||
ditto "${src_framework_dir}" "${dst_framework}"
|
|
||||||
|
|
||||||
# Prefer the canonical framework binary path.
|
|
||||||
dst_bin="${dst_framework}/Versions/Current/Sparkle"
|
|
||||||
if [[ ! -e "${dst_bin}" ]]; then
|
|
||||||
echo "Error: Sparkle binary missing at ${dst_bin}" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
sparkle_rpath="@rpath/Sparkle.framework/Versions/Current/Sparkle"
|
|
||||||
|
|
||||||
# Sanity check: top-level Sparkle entry should be a symlink (not a copied Mach-O file).
|
|
||||||
if [[ -e "${dst_framework}/Sparkle" && ! -L "${dst_framework}/Sparkle" ]]; then
|
|
||||||
echo "Warning: ${dst_framework}/Sparkle is not a symlink (unexpected). This can confuse codesign." >&2
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Fixing Sparkle.framework install name"
|
|
||||||
install_name_tool -id "${sparkle_rpath}" "${dst_bin}"
|
|
||||||
|
|
||||||
echo "Ensuring main binary has Frameworks rpath"
|
|
||||||
install_name_tool -add_rpath "@executable_path/../Frameworks" "${main_bin}" || true
|
|
||||||
|
|
||||||
echo "Rewriting Sparkle.framework references to @rpath"
|
|
||||||
# Try to rewrite a few common Homebrew Sparkle install names as well, because the
|
|
||||||
# recorded install name may differ from the path returned by CMake's find_library.
|
|
||||||
old_candidates=(
|
|
||||||
"${sparkle_bin_link}"
|
|
||||||
"${sparkle_bin_real}"
|
|
||||||
"/opt/homebrew/opt/sparkle-framework/Frameworks/Sparkle.framework/Versions/A/Sparkle"
|
|
||||||
"/opt/homebrew/opt/sparkle-framework/Frameworks/Sparkle.framework/Versions/B/Sparkle"
|
|
||||||
"/opt/homebrew/Frameworks/Sparkle.framework/Versions/A/Sparkle"
|
|
||||||
"/opt/homebrew/Frameworks/Sparkle.framework/Versions/B/Sparkle"
|
|
||||||
"/usr/local/opt/sparkle-framework/Frameworks/Sparkle.framework/Versions/A/Sparkle"
|
|
||||||
"/usr/local/opt/sparkle-framework/Frameworks/Sparkle.framework/Versions/B/Sparkle"
|
|
||||||
"/usr/local/Frameworks/Sparkle.framework/Versions/A/Sparkle"
|
|
||||||
"/usr/local/Frameworks/Sparkle.framework/Versions/B/Sparkle"
|
|
||||||
)
|
|
||||||
|
|
||||||
for old in "${old_candidates[@]}"; do
|
|
||||||
if [[ -n "${old}" ]]; then
|
|
||||||
install_name_tool -change "${old}" "${sparkle_rpath}" "${main_bin}" || true
|
|
||||||
if [[ -f "${qtsparkle_dylib}" ]]; then
|
|
||||||
install_name_tool -change "${old}" "${sparkle_rpath}" "${qtsparkle_dylib}" || true
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
20
dist/macos/entitlements.mas.plist
vendored
20
dist/macos/entitlements.mas.plist
vendored
@@ -1,20 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<!-- Enable the App Sandbox (required for Mac App Store). -->
|
|
||||||
<key>com.apple.security.app-sandbox</key>
|
|
||||||
<true/>
|
|
||||||
|
|
||||||
<!-- Strawberry is a client app that needs outbound network access (streaming/scrobbling/etc). -->
|
|
||||||
<key>com.apple.security.network.client</key>
|
|
||||||
<true/>
|
|
||||||
|
|
||||||
<!-- Allow access to user-selected music folders/files (via NSOpenPanel security-scoped bookmarks). -->
|
|
||||||
<key>com.apple.security.files.user-selected.read-write</key>
|
|
||||||
<true/>
|
|
||||||
|
|
||||||
<!-- If iPod classic / other device access is rejected, we'll adjust entitlements after App Review feedback. -->
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
|
|
||||||
73
dist/macos/macdeploycheck.sh
vendored
73
dist/macos/macdeploycheck.sh
vendored
@@ -1,73 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# macdeploycheck: sanity check a deployed macOS .app bundle for accidental runtime deps
|
|
||||||
# on Homebrew/MacPorts paths (which break distribution / App Store / notarization).
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# macdeploycheck /path/to/App.app
|
|
||||||
|
|
||||||
app="${1:-}"
|
|
||||||
if [[ -z "$app" ]]; then
|
|
||||||
echo "Usage: macdeploycheck <path/to/App.app>" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
if [[ ! -d "$app" ]]; then
|
|
||||||
echo "Error: app bundle not found: $app" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
if [[ ! -d "$app/Contents" ]]; then
|
|
||||||
echo "Error: not a macOS app bundle (missing Contents/): $app" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
fail=0
|
|
||||||
tmp="$(mktemp -t macdeploycheck.XXXXXX)"
|
|
||||||
trap 'rm -f "$tmp"' EXIT
|
|
||||||
|
|
||||||
# Collect Mach-O files (executables + dylibs) inside the bundle.
|
|
||||||
while IFS= read -r -d '' f; do
|
|
||||||
if file "$f" | grep -q "Mach-O"; then
|
|
||||||
echo "$f" >>"$tmp"
|
|
||||||
fi
|
|
||||||
done < <(find "$app/Contents" -type f \( -perm -111 -o -name "*.dylib" -o -name "*.so" \) -print0 2>/dev/null)
|
|
||||||
|
|
||||||
if [[ ! -s "$tmp" ]]; then
|
|
||||||
echo "Warning: no Mach-O files found under $app/Contents" >&2
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "macdeploycheck: scanning for external (Homebrew/MacPorts) runtime deps..."
|
|
||||||
while IFS= read -r f; do
|
|
||||||
deps="$(otool -L "$f" 2>/dev/null | tail -n +2 | awk '{print $1}' || true)"
|
|
||||||
while IFS= read -r dep; do
|
|
||||||
[[ -z "$dep" ]] && continue
|
|
||||||
|
|
||||||
# Ignore system and rpath/loader/executable paths.
|
|
||||||
case "$dep" in
|
|
||||||
/System/*|/usr/lib/*|@rpath/*|@loader_path/*|@executable_path/*) continue ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# Common accidental runtime deps that will break distribution.
|
|
||||||
if [[ "$dep" == /opt/homebrew/* || "$dep" == /usr/local/* || "$dep" == /opt/local/* ]]; then
|
|
||||||
echo "ERROR: $f links to external path: $dep" >&2
|
|
||||||
fail=1
|
|
||||||
fi
|
|
||||||
done <<<"$deps"
|
|
||||||
done <"$tmp"
|
|
||||||
|
|
||||||
if [[ "$fail" -ne 0 ]]; then
|
|
||||||
cat >&2 <<'EOM'
|
|
||||||
|
|
||||||
One or more binaries in your .app link to a Homebrew (or MacPorts) path.
|
|
||||||
That usually means the bundle is not self-contained and will fail on other machines.
|
|
||||||
|
|
||||||
Fix: re-run your deploy step (e.g. macdeployqt) so frameworks/dylibs are bundled and
|
|
||||||
their install names are rewritten to @rpath/@loader_path.
|
|
||||||
EOM
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "OK: no external Homebrew/MacPorts runtime deps detected."
|
|
||||||
exit 0
|
|
||||||
|
|
||||||
22
dist/macos/macversion.sh
vendored
Executable file
22
dist/macos/macversion.sh
vendored
Executable file
@@ -0,0 +1,22 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
macos_version=$(sw_vers -productVersion)
|
||||||
|
macos_version_major=$(echo $macos_version | awk -F '[.]' '{print $1}')
|
||||||
|
macos_version_minor=$(echo $macos_version | awk -F '[.]' '{print $2}')
|
||||||
|
|
||||||
|
if [ "${macos_version_major}" = "10" ]; then
|
||||||
|
macos_codenames=(
|
||||||
|
["13"]="highsierra"
|
||||||
|
["14"]="mojave"
|
||||||
|
["15"]="catalina"
|
||||||
|
)
|
||||||
|
if [[ -n "${macos_codenames[$macos_version_minor]}" ]]; then
|
||||||
|
echo "${macos_codenames[$macos_version_minor]}"
|
||||||
|
else
|
||||||
|
echo "unknown"
|
||||||
|
fi
|
||||||
|
elif [ "${macos_version_major}" = "11" ]; then
|
||||||
|
echo "bigsur"
|
||||||
|
else
|
||||||
|
echo "unknown"
|
||||||
|
fi
|
||||||
80
dist/macos/privacy_policy.html
vendored
80
dist/macos/privacy_policy.html
vendored
@@ -1,80 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<title>Strawberry Music Player — Privacy Policy</title>
|
|
||||||
<style>
|
|
||||||
:root { color-scheme: light dark; }
|
|
||||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; margin: 24px; line-height: 1.45; }
|
|
||||||
main { max-width: 900px; margin: 0 auto; }
|
|
||||||
code { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
|
|
||||||
h1,h2 { line-height: 1.15; }
|
|
||||||
.muted { opacity: 0.75; }
|
|
||||||
ul { padding-left: 20px; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<main>
|
|
||||||
<h1>Privacy Policy</h1>
|
|
||||||
<p class="muted">Last updated: 2026-01-22</p>
|
|
||||||
|
|
||||||
<h2>Summary</h2>
|
|
||||||
<ul>
|
|
||||||
<li><strong>No analytics / tracking</strong>: This app does not include advertising SDKs, analytics SDKs, or tracking pixels.</li>
|
|
||||||
<li><strong>No data selling</strong>: We do not sell personal data.</li>
|
|
||||||
<li><strong>Optional online features</strong>: If you enable online features (lyrics lookup, cover art search, scrobbling, streaming services, radio, update checks), the app will contact third-party services and send the minimum data needed to provide the feature.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h2>What data is stored on your device</h2>
|
|
||||||
<p>Strawberry stores data locally on your device, such as:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Library database and playlists (file paths, track metadata, play counts, ratings).</li>
|
|
||||||
<li>App settings and preferences.</li>
|
|
||||||
<li>Optional service credentials/tokens you configure (for example scrobbling or streaming accounts), stored locally.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h2>What data is sent over the network (and when)</h2>
|
|
||||||
<p>Strawberry does not “phone home” just to run, but it will make network requests when you use or enable specific features. When the app contacts a third-party service, that service will receive standard network information such as your IP address, user-agent, and the request data described below.</p>
|
|
||||||
|
|
||||||
<h3>Album cover art search (optional)</h3>
|
|
||||||
<p>If you use album cover search (or enable “search automatically”), the app may send artist/album/track metadata to configured cover providers to find images.</p>
|
|
||||||
|
|
||||||
<h3>Lyrics lookup (optional)</h3>
|
|
||||||
<p>If you search for lyrics (or enable “search automatically”), the app may send artist/title/album/duration to configured lyrics providers to retrieve lyrics.</p>
|
|
||||||
|
|
||||||
<h3>Scrobbling (optional)</h3>
|
|
||||||
<p>If you enable scrobbling (for example Last.fm or ListenBrainz) the app sends “now playing” and/or listen history data to the configured scrobbling service, including track/artist/album metadata and timestamps. You can disable scrobbling at any time in Settings.</p>
|
|
||||||
|
|
||||||
<h3>Streaming services (optional)</h3>
|
|
||||||
<p>If you enable and sign into a streaming service (for example Tidal, Spotify, Qobuz, Subsonic-compatible servers), the app will communicate with that service to authenticate, browse, and play music. Requests may include account identifiers/tokens and media metadata required by the service.</p>
|
|
||||||
|
|
||||||
<h3>Internet radio (optional)</h3>
|
|
||||||
<p>If you use internet radio features, the app will contact the selected station/provider to retrieve station lists and stream audio.</p>
|
|
||||||
|
|
||||||
<h3>Discord Rich Presence (optional)</h3>
|
|
||||||
<p>If you enable Discord Rich Presence, the app shares currently playing track/artist/album information with the locally-running Discord client so it can be displayed on your Discord profile. You can disable this in Settings.</p>
|
|
||||||
|
|
||||||
<h3>Software updates</h3>
|
|
||||||
<p>The Mac App Store version of Strawberry is updated through Apple’s App Store.</p>
|
|
||||||
|
|
||||||
<h3>OAuth / local redirect server (optional)</h3>
|
|
||||||
<p>Some providers use an OAuth login flow that may open your browser and (in some cases) start a temporary local <code>http://localhost</code> redirect listener to complete authentication. This listener is local-only (not exposed on the internet) and only used during authentication. (Mac App Store builds may disable this flow.)</p>
|
|
||||||
|
|
||||||
<h2>Data sharing</h2>
|
|
||||||
<p>We do not share your personal data with third parties except as necessary to provide features you explicitly use or enable (for example, sending track metadata to a lyrics provider when you request lyrics).</p>
|
|
||||||
|
|
||||||
<h2>Your choices</h2>
|
|
||||||
<ul>
|
|
||||||
<li>Disable Discord Rich Presence in Settings.</li>
|
|
||||||
<li>Disable scrobbling services in Settings.</li>
|
|
||||||
<li>Disable automatic cover/lyrics searching in Settings.</li>
|
|
||||||
<li>Avoid signing into streaming services if you don’t want those network requests.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h2>Contact</h2>
|
|
||||||
<p>If you have questions about this policy, contact: <strong>privacy@dryark.com</strong></p>
|
|
||||||
</main>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
||||||
29
dist/scripts/import-from-clementine.sh
vendored
29
dist/scripts/import-from-clementine.sh
vendored
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
# Strawberry Music Player
|
# Strawberry Music Player
|
||||||
# Copyright 2020, Jonas Kvinge <jonas@jkvinge.net>
|
# Copyright 2020, Jonas Kvinge <jonas@jkvinge.net>
|
||||||
# Copyright 2021, Alexey Vazhnov
|
# 2021 Alexey Vazhnov
|
||||||
#
|
#
|
||||||
# Strawberry is free software: you can redistribute it and/or modify
|
# Strawberry is free software: you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU General Public License as published by
|
# it under the terms of the GNU General Public License as published by
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-only
|
# SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
|
|
||||||
# Based on https://wiki.strawberrymusicplayer.org/wiki/Import_collection_library_and_playlists_from_Clementine
|
# Based on https://github.com/strawberrymusicplayer/strawberry/wiki/Import-collection-library-and-playlists-data-from-Clementine
|
||||||
|
|
||||||
set -o nounset
|
set -o nounset
|
||||||
set -o errexit
|
set -o errexit
|
||||||
@@ -35,8 +35,8 @@ test -f "$FILE_DST" || { echo "No such file: $FILE_DST"; exit 1; }
|
|||||||
|
|
||||||
echo "Will try to copy information from $FILE_SRC to $FILE_DST."
|
echo "Will try to copy information from $FILE_SRC to $FILE_DST."
|
||||||
echo
|
echo
|
||||||
echo 'This script will **delete all data** from the Strawberry database!'
|
echo 'This script will **delete all information** from Strawberry database!'
|
||||||
read -r -p 'Do you want to continue? (Only YES is accepted) ' answer
|
read -r -p 'Do you want to continue? (the only YES is accepted) ' answer
|
||||||
if [ "$answer" != "YES" ]; then exit 1; fi
|
if [ "$answer" != "YES" ]; then exit 1; fi
|
||||||
|
|
||||||
# 'heredoc' with substitution of variables, see `man bash`, "Here Documents":
|
# 'heredoc' with substitution of variables, see `man bash`, "Here Documents":
|
||||||
@@ -62,9 +62,9 @@ INSERT INTO strawberry.subdirectories (directory_id, path, mtime) SELECT directo
|
|||||||
INSERT INTO strawberry.songs (ROWID, title, album, artist, albumartist, track, disc, year, originalyear, genre, compilation, composer, performer, grouping, comment, lyrics, beginning, length, bitrate, samplerate, directory_id, url, filetype, filesize, mtime, ctime, unavailable, playcount, skipcount, lastplayed, compilation_detected, compilation_on, compilation_off, compilation_effective, art_automatic, art_manual, effective_albumartist, effective_originalyear, cue_path, rating)
|
INSERT INTO strawberry.songs (ROWID, title, album, artist, albumartist, track, disc, year, originalyear, genre, compilation, composer, performer, grouping, comment, lyrics, beginning, length, bitrate, samplerate, directory_id, url, filetype, filesize, mtime, ctime, unavailable, playcount, skipcount, lastplayed, compilation_detected, compilation_on, compilation_off, compilation_effective, art_automatic, art_manual, effective_albumartist, effective_originalyear, cue_path, rating)
|
||||||
SELECT ROWID, title, album, artist, albumartist, track, disc, year, originalyear, genre, compilation, composer, performer, grouping, comment, lyrics, beginning, length, bitrate, samplerate, directory, filename, filetype, filesize, mtime, ctime, unavailable, playcount, skipcount, lastplayed, sampler, forced_compilation_on, forced_compilation_off, effective_compilation, art_automatic, art_manual, effective_albumartist, effective_originalyear, cue_path, rating FROM clementine.songs WHERE unavailable = 0;
|
SELECT ROWID, title, album, artist, albumartist, track, disc, year, originalyear, genre, compilation, composer, performer, grouping, comment, lyrics, beginning, length, bitrate, samplerate, directory, filename, filetype, filesize, mtime, ctime, unavailable, playcount, skipcount, lastplayed, sampler, forced_compilation_on, forced_compilation_off, effective_compilation, art_automatic, art_manual, effective_albumartist, effective_originalyear, cue_path, rating FROM clementine.songs WHERE unavailable = 0;
|
||||||
UPDATE strawberry.songs SET source = 2;
|
UPDATE strawberry.songs SET source = 2;
|
||||||
UPDATE strawberry.songs SET artist_id = '';
|
UPDATE strawberry.songs SET artist_id = "";
|
||||||
UPDATE strawberry.songs SET album_id = '';
|
UPDATE strawberry.songs SET album_id = "";
|
||||||
UPDATE strawberry.songs SET song_id = '';
|
UPDATE strawberry.songs SET song_id = "";
|
||||||
|
|
||||||
/* Import playlists */
|
/* Import playlists */
|
||||||
|
|
||||||
@@ -140,7 +140,7 @@ SELECT ROWID,
|
|||||||
bitrate,
|
bitrate,
|
||||||
samplerate,
|
samplerate,
|
||||||
directory,
|
directory,
|
||||||
CASE WHEN filename IS NULL THEN '' ELSE filename END,
|
filename,
|
||||||
filetype,
|
filetype,
|
||||||
filesize,
|
filesize,
|
||||||
mtime,
|
mtime,
|
||||||
@@ -162,9 +162,16 @@ SELECT ROWID,
|
|||||||
|
|
||||||
UPDATE strawberry.playlist_items SET source = 2;
|
UPDATE strawberry.playlist_items SET source = 2;
|
||||||
UPDATE strawberry.playlist_items SET type = 2;
|
UPDATE strawberry.playlist_items SET type = 2;
|
||||||
UPDATE strawberry.playlist_items SET artist_id = '';
|
UPDATE strawberry.playlist_items SET artist_id = "";
|
||||||
UPDATE strawberry.playlist_items SET album_id = '';
|
UPDATE strawberry.playlist_items SET album_id = "";
|
||||||
UPDATE strawberry.playlist_items SET song_id = '';
|
UPDATE strawberry.playlist_items SET song_id = "";
|
||||||
|
|
||||||
|
/* Recreate the FTS tables */
|
||||||
|
|
||||||
|
DELETE FROM strawberry.songs_fts;
|
||||||
|
INSERT INTO strawberry.songs_fts (ROWID, ftstitle, ftsalbum, ftsartist, ftsalbumartist, ftscomposer, ftsperformer, ftsgrouping, ftsgenre, ftscomment)
|
||||||
|
SELECT ROWID, title, album, artist, albumartist, composer, performer, grouping, genre, comment
|
||||||
|
FROM strawberry.songs;
|
||||||
|
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
<project_license>GPL-3.0+</project_license>
|
<project_license>GPL-3.0+</project_license>
|
||||||
<provides>
|
<provides>
|
||||||
<binary>strawberry</binary>
|
<binary>strawberry</binary>
|
||||||
|
<binary>strawberry-tagreader</binary>
|
||||||
</provides>
|
</provides>
|
||||||
<name>Strawberry Music Player</name>
|
<name>Strawberry Music Player</name>
|
||||||
<summary>A music player and collection organizer</summary>
|
<summary>A music player and collection organizer</summary>
|
||||||
@@ -14,7 +15,7 @@
|
|||||||
<developer id="net.jkvinge.jonas">
|
<developer id="net.jkvinge.jonas">
|
||||||
<name>Jonas Kvinge</name>
|
<name>Jonas Kvinge</name>
|
||||||
</developer>
|
</developer>
|
||||||
<translation type="qt">strawberry</translation>
|
<translation type="gettext">strawberry</translation>
|
||||||
<content_rating type="oars-1.1" />
|
<content_rating type="oars-1.1" />
|
||||||
<description>
|
<description>
|
||||||
<p>
|
<p>
|
||||||
@@ -31,10 +32,10 @@
|
|||||||
<li>Edit tags on audio files</li>
|
<li>Edit tags on audio files</li>
|
||||||
<li>Automatically retrieve tags from MusicBrainz</li>
|
<li>Automatically retrieve tags from MusicBrainz</li>
|
||||||
<li>Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify</li>
|
<li>Album cover art from Last.fm, Musicbrainz, Discogs, Musixmatch, Deezer, Tidal, Qobuz and Spotify</li>
|
||||||
<li>Lyrics from multiple sources</li>
|
<li>Song lyrics from Genius, Musixmatch, ChartLyrics, lyrics.ovh, lololyrics.com, songlyrics.com, azlyrics.com and elyrics.net</li>
|
||||||
<li>Audio analyzer and equalizer</li>
|
<li>Audio analyzer and equalizer</li>
|
||||||
<li>Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic</li>
|
<li>Transfer music to mass-storage USB players, MTP compatible devices and iPod Nano/Classic</li>
|
||||||
<li>Scrobbler with support for Last.fm and ListenBrainz</li>
|
<li>Scrobbler with support for Last.fm, Libre.fm and ListenBrainz</li>
|
||||||
<li>Streaming support for Subsonic-compatible servers</li>
|
<li>Streaming support for Subsonic-compatible servers</li>
|
||||||
<li>Unofficial streaming support for Tidal, Spotify and Qobuz</li>
|
<li>Unofficial streaming support for Tidal, Spotify and Qobuz</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -51,23 +52,6 @@
|
|||||||
</screenshots>
|
</screenshots>
|
||||||
<update_contact>eclipseo@fedoraproject.org</update_contact>
|
<update_contact>eclipseo@fedoraproject.org</update_contact>
|
||||||
<releases>
|
<releases>
|
||||||
<release version="1.2.17" date="2026-01-18"/>
|
|
||||||
<release version="1.2.16" date="2025-12-16"/>
|
|
||||||
<release version="1.2.15" date="2025-11-25"/>
|
|
||||||
<release version="1.2.14" date="2025-10-25"/>
|
|
||||||
<release version="1.2.13" date="2025-08-31"/>
|
|
||||||
<release version="1.2.12" date="2025-08-12"/>
|
|
||||||
<release version="1.2.11" date="2025-05-15"/>
|
|
||||||
<release version="1.2.10" date="2025-04-18"/>
|
|
||||||
<release version="1.2.9" date="2025-04-08"/>
|
|
||||||
<release version="1.2.8" date="2025-04-05"/>
|
|
||||||
<release version="1.2.7" date="2025-01-31"/>
|
|
||||||
<release version="1.2.6" date="2025-01-17"/>
|
|
||||||
<release version="1.2.5" date="2025-01-17"/>
|
|
||||||
<release version="1.2.4" date="2025-01-10"/>
|
|
||||||
<release version="1.2.3" date="2024-12-08"/>
|
|
||||||
<release version="1.2.2" date="2024-11-23"/>
|
|
||||||
<release version="1.2.1" date="2024-11-21"/>
|
|
||||||
<release version="1.1.3" date="2024-09-21"/>
|
<release version="1.1.3" date="2024-09-21"/>
|
||||||
<release version="1.1.2" date="2024-09-12"/>
|
<release version="1.1.2" date="2024-09-12"/>
|
||||||
<release version="1.1.1" date="2024-07-22"/>
|
<release version="1.1.1" date="2024-07-22"/>
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ TryExec=strawberry
|
|||||||
Icon=strawberry
|
Icon=strawberry
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Categories=AudioVideo;Player;Qt;Audio;
|
Categories=AudioVideo;Player;Qt;Audio;
|
||||||
Keywords=Audio;Player;Clementine;
|
Keywords=Audio;Player;
|
||||||
|
StartupNotify=false
|
||||||
MimeType=x-content/audio-player;application/ogg;application/x-ogg;application/x-ogm-audio;audio/flac;audio/ogg;audio/vorbis;audio/aac;audio/mp4;audio/mpeg;audio/mpegurl;audio/vnd.rn-realaudio;audio/x-flac;audio/x-oggflac;audio/x-vorbis;audio/x-vorbis+ogg;audio/x-speex;audio/x-wav;audio/x-wavpack;audio/x-ape;audio/x-mp3;audio/x-mpeg;audio/x-mpegurl;audio/x-ms-wma;audio/x-musepack;audio/x-pn-realaudio;audio/x-scpls;video/x-ms-asf;x-scheme-handler/tidal;
|
MimeType=x-content/audio-player;application/ogg;application/x-ogg;application/x-ogm-audio;audio/flac;audio/ogg;audio/vorbis;audio/aac;audio/mp4;audio/mpeg;audio/mpegurl;audio/vnd.rn-realaudio;audio/x-flac;audio/x-oggflac;audio/x-vorbis;audio/x-vorbis+ogg;audio/x-speex;audio/x-wav;audio/x-wavpack;audio/x-ape;audio/x-mp3;audio/x-mpeg;audio/x-mpegurl;audio/x-ms-wma;audio/x-musepack;audio/x-pn-realaudio;audio/x-scpls;video/x-ms-asf;x-scheme-handler/tidal;
|
||||||
StartupWMClass=strawberry
|
StartupWMClass=strawberry
|
||||||
Actions=Play-Pause;Stop;StopAfterCurrent;Previous;Next;
|
Actions=Play-Pause;Stop;StopAfterCurrent;Previous;Next;
|
||||||
|
|||||||
10
dist/unix/strawberry-tagreader.1
vendored
Normal file
10
dist/unix/strawberry-tagreader.1
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
.TH STRAWBERRY-TAGREADER "1"
|
||||||
|
.SH NAME
|
||||||
|
strawberry-tagreader \- internal tag reader for strawberry
|
||||||
|
.SH SYNOPSIS
|
||||||
|
.B strawberry-tagreader
|
||||||
|
.SH DESCRIPTION
|
||||||
|
This program is used internally by Strawberry to parse tags in music files without exposing the whole application to crashes caused by malformed files. It is not meant to be run on its own.
|
||||||
|
.SH "AUTHORS"
|
||||||
|
.PP
|
||||||
|
Strawberry main developer is Jonas Kvinge <jonas@jkvinge.net>.
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user