diff --git a/CREDITS.md b/CREDITS.md index a76862d..cc6c61b 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -1,7 +1,9 @@ # Contributors -- Peter Sharma and contributors: application and controller protocol implementation. +- [Peter Sharma and contributors](https://github.com/Peterksharma/switch2mac): application and controller protocol implementation. - Andrei-Kondrykau: browser integration. - vialoh: RetroArch integration. - ndeadly, Nadeflore, coffincolors, trevlars, darthcloud, and the controller research community: protocol research. - Sam Lantinga and the SDL contributors: SDL. + +License notices: [LICENSES/README.md](LICENSES/README.md). diff --git a/Integrations/CMake/Bundle.cmake b/Integrations/CMake/Bundle.cmake index 11e066a..3650dc6 100644 --- a/Integrations/CMake/Bundle.cmake +++ b/Integrations/CMake/Bundle.cmake @@ -22,6 +22,9 @@ function(switch2kit_embed target) set_property(TARGET "${target}" PROPERTY MACOSX_BUNDLE_INFO_PLIST "${_prepared}") endif() + get_filename_component(_notice_root "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/../.." ABSOLUTE) + file(GLOB _notices CONFIGURE_DEPENDS "${_notice_root}/LICENSES/*") + set_property(TARGET "${target}" APPEND PROPERTY LINK_DEPENDS "${_notice_root}/CREDITS.md" ${_notices}) add_custom_command(TARGET "${target}" POST_BUILD COMMAND "${CMAKE_COMMAND}" -E make_directory "$/Frameworks" COMMAND "${CMAKE_COMMAND}" -E copy_if_different "$" @@ -29,6 +32,12 @@ function(switch2kit_embed target) COMMAND "${CMAKE_COMMAND}" "-DS2K_BUNDLE_LIBRARY=$/Frameworks/$" -P "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/PrepareBundle.cmake" + COMMAND "${CMAKE_COMMAND}" -E make_directory + "$/Resources/Switch2KitNotices" + COMMAND "${CMAKE_COMMAND}" -E copy_if_different "${_notice_root}/CREDITS.md" + "$/Resources/Switch2KitNotices/CREDITS.md" + COMMAND "${CMAKE_COMMAND}" -E copy_directory "${_notice_root}/LICENSES" + "$/Resources/Switch2KitNotices/LICENSES" COMMAND xcrun swift-stdlib-tool --copy --platform macosx --scan-executable "$" --destination "$/Frameworks" diff --git a/LICENSES/MIT-trevlars.txt b/LICENSES/MIT-trevlars.txt new file mode 100644 index 0000000..4db4b32 --- /dev/null +++ b/LICENSES/MIT-trevlars.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 trevlars + +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. diff --git a/LICENSES/README.md b/LICENSES/README.md new file mode 100644 index 0000000..a55a673 --- /dev/null +++ b/LICENSES/README.md @@ -0,0 +1,8 @@ +# Third-party notices + +Switch2Kit includes code from [Peterksharma/switch2mac](https://github.com/Peterksharma/switch2mac/tree/ea6719f0a1d6b6986c00aca9ed4169a85c8cc9ae). Contributors are listed in [CREDITS.md](../CREDITS.md). + +- [MIT-trevlars.txt](MIT-trevlars.txt): controller reference implementation, [trevlars/switch2-controllers-linux](https://github.com/trevlars/switch2-controllers-linux). +- [SDL-zlib.txt](SDL-zlib.txt): SDL-derived patches, [SDL 3.4.14](https://github.com/libsdl-org/SDL/tree/147a8ee32dbf9ac02f3794964490687b6bbda1bc). These are modified, unofficial SDL sources. + +Dolphin and Cemu retain their upstream copyright and license notices. diff --git a/LICENSES/SDL-zlib.txt b/LICENSES/SDL-zlib.txt new file mode 100644 index 0000000..acaf5b2 --- /dev/null +++ b/LICENSES/SDL-zlib.txt @@ -0,0 +1,17 @@ +Copyright (C) 1997-2026 Sam Lantinga + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. diff --git a/Resources/Info.plist b/Resources/Info.plist index 9b1013d..a95d70e 100644 --- a/Resources/Info.plist +++ b/Resources/Info.plist @@ -23,6 +23,6 @@ NSBluetoothAlwaysUsageDescription Switch2Kit talks to your Nintendo Switch 2 controllers over Bluetooth so games on this Mac can use them. NSHumanReadableCopyright - © 2026 Peter Sharma. Protocol research by the Switch 2 controller community. + © 2026 Peter Sharma and Switch2Kit contributors. diff --git a/scripts/build-app.sh b/scripts/build-app.sh index 4ef9c8c..e1dab59 100755 --- a/scripts/build-app.sh +++ b/scripts/build-app.sh @@ -41,12 +41,17 @@ if [ -f Resources/AppIcon.icns ]; then "$OUT/Contents/Info.plist" 2>/dev/null || true fi +# Keep the compact source notices in the bundle before any signature is applied. +mkdir -p "$OUT/Contents/Resources/Switch2KitNotices" +cp CREDITS.md "$OUT/Contents/Resources/Switch2KitNotices/" +cp -R LICENSES "$OUT/Contents/Resources/Switch2KitNotices/" + # Keep setup resources with the binary; never install the extension automatically. mkdir -p "$OUT/Contents/Resources/BrowserExtension" cp browser/extension/manifest.json browser/extension/*.js "$OUT/Contents/Resources/BrowserExtension/" REVISION=$(git rev-parse HEAD) DIRTY=false -[ -z "$(git status --porcelain --untracked-files=normal -- Sources Resources browser/extension scripts Package.swift)" ] || DIRTY=true +[ -z "$(git status --porcelain --untracked-files=normal -- Sources Resources browser/extension scripts Package.swift CREDITS.md LICENSES)" ] || DIRTY=true /usr/libexec/PlistBuddy -c "Add :Switch2KitSourceRevision string $REVISION" "$OUT/Contents/Info.plist" /usr/libexec/PlistBuddy -c "Add :Switch2KitSourceDirty bool $DIRTY" "$OUT/Contents/Info.plist" /usr/libexec/PlistBuddy -c "Add :Switch2KitBuildArchitecture string $(uname -m)" "$OUT/Contents/Info.plist" diff --git a/scripts/build-switch2kit-c.sh b/scripts/build-switch2kit-c.sh index a4a5987..7c27f46 100755 --- a/scripts/build-switch2kit-c.sh +++ b/scripts/build-switch2kit-c.sh @@ -60,6 +60,9 @@ lipo "$WORK/consumer" -verify_arch arm64 x86_64 "$WORK/consumer" xcodebuild -create-xcframework -library "$LIB" \ -headers "$ROOT/Sources/Switch2KitCABI/include" -output "$WORK/Switch2KitC.xcframework" +mkdir -p "$WORK/Switch2KitC.xcframework/Notices" +cp "$ROOT/CREDITS.md" "$WORK/Switch2KitC.xcframework/Notices/" +cp -R "$ROOT/LICENSES" "$WORK/Switch2KitC.xcframework/Notices/" plutil -lint "$WORK/Switch2KitC.xcframework/Info.plist" python3 - "$WORK/Switch2KitC.xcframework/Info.plist" <<'PY' import plistlib,sys diff --git a/scripts/build-switch2kit-emulator.sh b/scripts/build-switch2kit-emulator.sh index 10e4603..8b0c176 100755 --- a/scripts/build-switch2kit-emulator.sh +++ b/scripts/build-switch2kit-emulator.sh @@ -17,3 +17,4 @@ case "$emulator" in esac cmake -S "$source" -B "$build" "${args[@]}" "$@" cmake --build "$build" --target "$target" --parallel "${S2K_BUILD_JOBS:-4}" +python3 "$root/scripts/verify-distribution-notices.py" "$emulator" "$source" "$build" diff --git a/scripts/verify-distribution-notices.py b/scripts/verify-distribution-notices.py new file mode 100644 index 0000000..d909593 --- /dev/null +++ b/scripts/verify-distribution-notices.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +"""Check bundled source notices and upstream Finder metadata.""" +import argparse +import hashlib +import json +from pathlib import Path +import plistlib + +ROOT = Path(__file__).resolve().parents[1] +NOTICES = ('CREDITS.md', 'LICENSES/README.md', 'LICENSES/MIT-trevlars.txt', 'LICENSES/SDL-zlib.txt') + + +def verify(app, emulator, root=ROOT): + info = plistlib.loads((app / 'Contents/Info.plist').read_bytes()) + expected_id = {'dolphin': 'org.dolphin-emu.dolphin', 'cemu': 'info.cemu.Cemu'}[emulator] + if info.get('CFBundleIdentifier') != expected_id: + raise ValueError('Unexpected host bundle identifier') + notice = info.get('NSHumanReadableCopyright') + if not isinstance(notice, str) or not notice.strip() or not ( + 'GPL' in notice if emulator == 'dolphin' else 'Cemu' in notice): + raise ValueError('Missing or replaced upstream Finder copyright/license notice') + digests = {} + for name in NOTICES: + source = root / name + bundled = app / 'Contents/Resources/Switch2KitNotices' / name + if not bundled.is_file() or bundled.is_symlink() or bundled.read_bytes() != source.read_bytes(): + raise ValueError('Missing or altered bundled notice: ' + name) + digests[name] = hashlib.sha256(source.read_bytes()).hexdigest() + return {'version': 1, 'emulator': emulator, 'copyright_notice': notice, + 'notices': digests} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('emulator', choices=('dolphin', 'cemu')) + parser.add_argument('source', type=Path) + parser.add_argument('build', type=Path) + options = parser.parse_args() + report = options.build / 'integration-notices.json' + report.unlink(missing_ok=True) + directory = options.build / 'Binaries' if options.emulator == 'dolphin' else options.source / 'bin' + apps = [app for app in directory.glob('*.app') + if (app / 'Contents/Frameworks/libSwitch2KitC.dylib').is_file()] + if len(apps) != 1: + raise ValueError('Expected exactly one integrated application') + result = verify(apps[0], options.emulator) + report.write_text(json.dumps(result, indent=2) + '\n') + print('PASS bundled notices and upstream Finder metadata') + + +if __name__ == '__main__': + main() diff --git a/tests/distribution-notices/run.sh b/tests/distribution-notices/run.sh new file mode 100755 index 0000000..ee98feb --- /dev/null +++ b/tests/distribution-notices/run.sh @@ -0,0 +1,3 @@ +#!/bin/bash +set -euo pipefail +python3 "$(dirname "$0")/test_notices.py" diff --git a/tests/distribution-notices/test_notices.py b/tests/distribution-notices/test_notices.py new file mode 100644 index 0000000..ec61187 --- /dev/null +++ b/tests/distribution-notices/test_notices.py @@ -0,0 +1,94 @@ +import importlib.util +import hashlib +from pathlib import Path +import plistlib +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[2] +spec = importlib.util.spec_from_file_location('notices', ROOT / 'scripts/verify-distribution-notices.py') +notices = importlib.util.module_from_spec(spec) +spec.loader.exec_module(notices) + + +class NoticesTests(unittest.TestCase): + def fixture(self, base, emulator): + app = base / 'Host with spaces.app' + for name in notices.NOTICES: + target = app / 'Contents/Resources/Switch2KitNotices' / name + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes((ROOT / name).read_bytes()) + info = {'CFBundleIdentifier': 'org.dolphin-emu.dolphin' if emulator == 'dolphin' else 'info.cemu.Cemu', + 'NSHumanReadableCopyright': 'Licensed under GPL version 2 or later (GPLv2+)' if emulator == 'dolphin' + else 'Copyright © 2026 Cemu Project'} + (app / 'Contents/Info.plist').write_bytes(plistlib.dumps(info)) + return app, info + + def test_both_upstream_notices_and_all_source_texts_are_retained(self): + for emulator in ('dolphin', 'cemu'): + with tempfile.TemporaryDirectory() as temporary: + app, info = self.fixture(Path(temporary), emulator) + result = notices.verify(app, emulator) + self.assertEqual(result['copyright_notice'], info['NSHumanReadableCopyright']) + self.assertEqual(set(result['notices']), set(notices.NOTICES)) + self.assertEqual(result['emulator'], emulator) + for name, digest in result['notices'].items(): + self.assertEqual(digest, hashlib.sha256((ROOT / name).read_bytes()).hexdigest()) + + def test_missing_empty_or_reassigned_copyright_fails(self): + for emulator in ('dolphin', 'cemu'): + for value in (None, '', ' ', 42, '© Switch2Kit'): + with self.subTest(emulator=emulator, value=value), tempfile.TemporaryDirectory() as temporary: + app, info = self.fixture(Path(temporary), emulator) + if value is None: + del info['NSHumanReadableCopyright'] + else: + info['NSHumanReadableCopyright'] = value + (app / 'Contents/Info.plist').write_bytes(plistlib.dumps(info)) + with self.assertRaises(ValueError): + notices.verify(app, emulator) + + def test_missing_altered_or_symlinked_notice_fails(self): + for name in notices.NOTICES: + for defect in ('missing', 'changed', 'symlink'): + with self.subTest(name=name, defect=defect), tempfile.TemporaryDirectory() as temporary: + app, _ = self.fixture(Path(temporary), 'dolphin') + target = app / 'Contents/Resources/Switch2KitNotices' / name + target.unlink() + if defect == 'changed': + target.write_text('Not the retained notice') + elif defect == 'symlink': + target.symlink_to(ROOT / name) + with self.assertRaises(ValueError): + notices.verify(app, 'dolphin') + + def test_emulator_identity_cannot_be_reassigned(self): + with tempfile.TemporaryDirectory() as temporary: + app, _ = self.fixture(Path(temporary), 'dolphin') + with self.assertRaises(ValueError): + notices.verify(app, 'cemu') + + def test_packagers_copy_notices_before_signing_or_archiving(self): + app = (ROOT / 'scripts/build-app.sh').read_text() + self.assertLess(app.index('cp -R LICENSES'), app.index('codesign --force')) + native = (ROOT / 'scripts/build-switch2kit-c.sh').read_text() + self.assertLess(native.index('cp -R "$ROOT/LICENSES"'), native.index('ditto -c -k')) + bundle = (ROOT / 'Integrations/CMake/Bundle.cmake').read_text() + self.assertIn('LINK_DEPENDS "${_notice_root}/CREDITS.md"', bundle) + self.assertIn('copy_directory "${_notice_root}/LICENSES"', bundle) + self.assertNotIn('COMMAND codesign', bundle) + self.assertIn('verify-distribution-notices.py', (ROOT / 'scripts/build-switch2kit-emulator.sh').read_text()) + + def test_notice_index_links_sources_and_retained_texts(self): + text = (ROOT / 'LICENSES/README.md').read_text() + self.assertIn('https://github.com/Peterksharma/switch2mac/tree/', text) + self.assertIn('[CREDITS.md](../CREDITS.md)', text) + for name in ('MIT-trevlars.txt', 'SDL-zlib.txt'): + self.assertIn('(' + name + ')', text) + self.assertIn('modified, unofficial SDL sources', text) + self.assertIn('Permission is hereby granted', (ROOT / 'LICENSES/MIT-trevlars.txt').read_text()) + self.assertIn('This notice may not be removed', (ROOT / 'LICENSES/SDL-zlib.txt').read_text()) + + +if __name__ == '__main__': + unittest.main()