diff --git a/.gitignore b/.gitignore index 17aa0aa..9dbfc12 100644 --- a/.gitignore +++ b/.gitignore @@ -69,3 +69,17 @@ initramfs/system/lib/liboscortex_libc.so iso_root_aarch64/ oscortex-aarch64.iso tools/flutter-engine/linux-arm64/ +apps/_x86_aot_backup/ +*.iso +iso_root/ +*.kernel + +# Dart/Flutter package build artifacts +packages/*/.dart_tool/ +packages/*/pubspec.lock +packages/*/.flutter-plugins* +packages/*/build/ + +# osx CLI per-app build config + outputs +apps/*/.osx/ +apps/*/build/osx/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..559d1ae --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,30 @@ +# OSCortex — agent instructions + +OSCortex is a from-scratch OS: a Rust microkernel (x86_64 + aarch64, no glibc/std, +software-composited framebuffer, no GPU) running a custom prebuilt Flutter embedder +as the shell + apps. Single source tree; active branch `feat/native-engine-port`, +base `develop`. + +## ‼️ READ `rules.md` BEFORE STARTING AND BEFORE REPORTING DONE — MANDATORY +[rules.md](rules.md) is binding for every agent. The short version: +- **Never present a stub/placeholder/mock as done.** Label it in code, report it as a + stub, and never call it "done/working/complete". +- **"Done" = verified end-to-end on the REAL artifact** (the actual ISO/binary the + user runs), with saved proof. Compiles/renders/tests-pass ≠ done. +- **Report status as exactly one of: DONE / UNVERIFIED / SCAFFOLD-STUB / NOT STARTED.** +- **Finish the task or name the gap precisely** (what's missing, why, effort). No + silent scope-narrowing. +- Keep [docs/FEATURE_STATUS.md](docs/FEATURE_STATUS.md) honest and current. + +Violating these wastes the maintainer's time and breaks trust. Slower-but-true beats +fast-but-fake, always. + +## Build / test (x86, the primary target) +- Release build with the SMP app-launch fix: `KERNEL_FEATURES=smp X86_AOT=1 SKIP_CORE_APPS=1 bash scripts/build-iso.sh` → `oscortex.iso`. Add `input-hud` to KERNEL_FEATURES only for local debugging (never in a release). +- The SMP app-launch fix is inert on a single core — test under `-smp 2`+. +- Verify on the real ISO before claiming anything works (Rule 5). + +## Hard constraints +- **No Claude/Anthropic attribution** on commits or anything cloud-bound. +- Only verified work goes to cloud. Published == locally verified. +- Never enter the maintainer's credentials/tokens — that is the maintainer's action. diff --git a/apps/oscortex_app/ios/.gitignore b/apps/oscortex_app/ios/.gitignore deleted file mode 100644 index 7a7f987..0000000 --- a/apps/oscortex_app/ios/.gitignore +++ /dev/null @@ -1,34 +0,0 @@ -**/dgph -*.mode1v3 -*.mode2v3 -*.moved-aside -*.pbxuser -*.perspectivev3 -**/*sync/ -.sconsign.dblite -.tags* -**/.vagrant/ -**/DerivedData/ -Icon? -**/Pods/ -**/.symlinks/ -profile -xcuserdata -**/.generated/ -Flutter/App.framework -Flutter/Flutter.framework -Flutter/Flutter.podspec -Flutter/Generated.xcconfig -Flutter/ephemeral/ -Flutter/app.flx -Flutter/app.zip -Flutter/flutter_assets/ -Flutter/flutter_export_environment.sh -ServiceDefinitions.json -Runner/GeneratedPluginRegistrant.* - -# Exceptions to above rules. -!default.mode1v3 -!default.mode2v3 -!default.pbxuser -!default.perspectivev3 diff --git a/apps/oscortex_app/ios/Flutter/AppFrameworkInfo.plist b/apps/oscortex_app/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100644 index 391a902..0000000 --- a/apps/oscortex_app/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,24 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - - diff --git a/apps/oscortex_app/ios/Flutter/Debug.xcconfig b/apps/oscortex_app/ios/Flutter/Debug.xcconfig deleted file mode 100644 index 592ceee..0000000 --- a/apps/oscortex_app/ios/Flutter/Debug.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "Generated.xcconfig" diff --git a/apps/oscortex_app/ios/Flutter/Release.xcconfig b/apps/oscortex_app/ios/Flutter/Release.xcconfig deleted file mode 100644 index 592ceee..0000000 --- a/apps/oscortex_app/ios/Flutter/Release.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "Generated.xcconfig" diff --git a/apps/oscortex_app/ios/Runner.xcodeproj/project.pbxproj b/apps/oscortex_app/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index 89eba7c..0000000 --- a/apps/oscortex_app/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,623 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 54; - objects = { - -/* Begin PBXBuildFile section */ - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; - 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; }; - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 97C146E61CF9000F007C117D /* Project object */; - proxyType = 1; - remoteGlobalIDString = 97C146ED1CF9000F007C117D; - remoteInfo = Runner; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 9705A1C41CF9048500538489 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; - 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; - 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; - 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 331C8082294A63A400263BE5 /* RunnerTests */ = { - isa = PBXGroup; - children = ( - 331C807B294A618700263BE5 /* RunnerTests.swift */, - ); - path = RunnerTests; - sourceTree = ""; - }; - 9740EEB11CF90186004384FC /* Flutter */ = { - isa = PBXGroup; - children = ( - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 9740EEB31CF90195004384FC /* Generated.xcconfig */, - ); - name = Flutter; - sourceTree = ""; - }; - 97C146E51CF9000F007C117D = { - isa = PBXGroup; - children = ( - 9740EEB11CF90186004384FC /* Flutter */, - 97C146F01CF9000F007C117D /* Runner */, - 97C146EF1CF9000F007C117D /* Products */, - 331C8082294A63A400263BE5 /* RunnerTests */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - 331C8081294A63A400263BE5 /* RunnerTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 97C146FA1CF9000F007C117D /* Main.storyboard */, - 97C146FD1CF9000F007C117D /* Assets.xcassets */, - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, - 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, - 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */, - 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, - ); - path = Runner; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 331C8080294A63A400263BE5 /* RunnerTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; - buildPhases = ( - 331C807D294A63A400263BE5 /* Sources */, - 331C807F294A63A400263BE5 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 331C8086294A63A400263BE5 /* PBXTargetDependency */, - ); - name = RunnerTests; - productName = RunnerTests; - productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Runner; - productName = Runner; - productReference = 97C146EE1CF9000F007C117D /* Runner.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 97C146E61CF9000F007C117D /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = YES; - LastUpgradeCheck = 1510; - ORGANIZATIONNAME = ""; - TargetAttributes = { - 331C8080294A63A400263BE5 = { - CreatedOnToolsVersion = 14.0; - TestTargetID = 97C146ED1CF9000F007C117D; - }; - 97C146ED1CF9000F007C117D = { - CreatedOnToolsVersion = 7.3.1; - LastSwiftMigration = 1100; - }; - }; - }; - buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 97C146E51CF9000F007C117D; - productRefGroup = 97C146EF1CF9000F007C117D /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 97C146ED1CF9000F007C117D /* Runner */, - 331C8080294A63A400263BE5 /* RunnerTests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 331C807F294A63A400263BE5 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 97C146EC1CF9000F007C117D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 331C807D294A63A400263BE5 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 97C146ED1CF9000F007C117D /* Runner */; - targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 249021D3217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Profile; - }; - 249021D4217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = NNXKAV4XD5; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.oscortexApp; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Profile; - }; - 331C8088294A63A400263BE5 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.oscortexApp.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; - }; - name = Debug; - }; - 331C8089294A63A400263BE5 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.oscortexApp.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; - }; - name = Release; - }; - 331C808A294A63A400263BE5 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.oscortexApp.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; - }; - name = Profile; - }; - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = NNXKAV4XD5; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.oscortexApp; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Debug; - }; - 97C147071CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = NNXKAV4XD5; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.oscortexApp; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 331C8088294A63A400263BE5 /* Debug */, - 331C8089294A63A400263BE5 /* Release */, - 331C808A294A63A400263BE5 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147031CF9000F007C117D /* Debug */, - 97C147041CF9000F007C117D /* Release */, - 249021D3217E4FDB00AE95B9 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147061CF9000F007C117D /* Debug */, - 97C147071CF9000F007C117D /* Release */, - 249021D4217E4FDB00AE95B9 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 97C146E61CF9000F007C117D /* Project object */; -} diff --git a/apps/oscortex_app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/apps/oscortex_app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 919434a..0000000 --- a/apps/oscortex_app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/apps/oscortex_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/apps/oscortex_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d9810..0000000 --- a/apps/oscortex_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/apps/oscortex_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/apps/oscortex_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100644 index f9b0d7c..0000000 --- a/apps/oscortex_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - PreviewsEnabled - - - diff --git a/apps/oscortex_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/apps/oscortex_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index e3773d4..0000000 --- a/apps/oscortex_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,101 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/oscortex_app/ios/Runner.xcworkspace/contents.xcworkspacedata b/apps/oscortex_app/ios/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 1d526a1..0000000 --- a/apps/oscortex_app/ios/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/apps/oscortex_app/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/apps/oscortex_app/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d9810..0000000 --- a/apps/oscortex_app/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/apps/oscortex_app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/apps/oscortex_app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100644 index f9b0d7c..0000000 --- a/apps/oscortex_app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - PreviewsEnabled - - - diff --git a/apps/oscortex_app/ios/Runner/AppDelegate.swift b/apps/oscortex_app/ios/Runner/AppDelegate.swift deleted file mode 100644 index c30b367..0000000 --- a/apps/oscortex_app/ios/Runner/AppDelegate.swift +++ /dev/null @@ -1,16 +0,0 @@ -import Flutter -import UIKit - -@main -@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { - override func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? - ) -> Bool { - return super.application(application, didFinishLaunchingWithOptions: launchOptions) - } - - func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { - GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) - } -} diff --git a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index d36b1fa..0000000 --- a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" - }, - { - "size" : "1024x1024", - "idiom" : "ios-marketing", - "filename" : "Icon-App-1024x1024@1x.png", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png deleted file mode 100644 index dc9ada4..0000000 Binary files a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and /dev/null differ diff --git a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png deleted file mode 100644 index 7353c41..0000000 Binary files a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and /dev/null differ diff --git a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png deleted file mode 100644 index 797d452..0000000 Binary files a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and /dev/null differ diff --git a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100644 index 6ed2d93..0000000 Binary files a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and /dev/null differ diff --git a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png deleted file mode 100644 index 4cd7b00..0000000 Binary files a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and /dev/null differ diff --git a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100644 index fe73094..0000000 Binary files a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and /dev/null differ diff --git a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png deleted file mode 100644 index 321773c..0000000 Binary files a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and /dev/null differ diff --git a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png deleted file mode 100644 index 797d452..0000000 Binary files a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and /dev/null differ diff --git a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100644 index 502f463..0000000 Binary files a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and /dev/null differ diff --git a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png deleted file mode 100644 index 0ec3034..0000000 Binary files a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and /dev/null differ diff --git a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100644 index 0ec3034..0000000 Binary files a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and /dev/null differ diff --git a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100644 index e9f5fea..0000000 Binary files a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and /dev/null differ diff --git a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100644 index 84ac32a..0000000 Binary files a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and /dev/null differ diff --git a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png deleted file mode 100644 index 8953cba..0000000 Binary files a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and /dev/null differ diff --git a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png deleted file mode 100644 index 0467bf1..0000000 Binary files a/apps/oscortex_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and /dev/null differ diff --git a/apps/oscortex_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/apps/oscortex_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json deleted file mode 100644 index 0bedcf2..0000000 --- a/apps/oscortex_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "LaunchImage.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "LaunchImage@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "LaunchImage@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/apps/oscortex_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/apps/oscortex_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png deleted file mode 100644 index 9da19ea..0000000 Binary files a/apps/oscortex_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png and /dev/null differ diff --git a/apps/oscortex_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/apps/oscortex_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png deleted file mode 100644 index 9da19ea..0000000 Binary files a/apps/oscortex_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png and /dev/null differ diff --git a/apps/oscortex_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/apps/oscortex_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png deleted file mode 100644 index 9da19ea..0000000 Binary files a/apps/oscortex_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png and /dev/null differ diff --git a/apps/oscortex_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/apps/oscortex_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md deleted file mode 100644 index 89c2725..0000000 --- a/apps/oscortex_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Launch Screen Assets - -You can customize the launch screen with your own desired assets by replacing the image files in this directory. - -You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/apps/oscortex_app/ios/Runner/Base.lproj/LaunchScreen.storyboard b/apps/oscortex_app/ios/Runner/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index f2e259c..0000000 --- a/apps/oscortex_app/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/oscortex_app/ios/Runner/Base.lproj/Main.storyboard b/apps/oscortex_app/ios/Runner/Base.lproj/Main.storyboard deleted file mode 100644 index f3c2851..0000000 --- a/apps/oscortex_app/ios/Runner/Base.lproj/Main.storyboard +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/oscortex_app/ios/Runner/Info.plist b/apps/oscortex_app/ios/Runner/Info.plist deleted file mode 100644 index 42a6122..0000000 --- a/apps/oscortex_app/ios/Runner/Info.plist +++ /dev/null @@ -1,70 +0,0 @@ - - - - - CADisableMinimumFrameDurationOnPhone - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - Oscortex App - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - oscortex_app - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSRequiresIPhoneOS - - UIApplicationSceneManifest - - UIApplicationSupportsMultipleScenes - - UISceneConfigurations - - UIWindowSceneSessionRoleApplication - - - UISceneClassName - UIWindowScene - UISceneConfigurationName - flutter - UISceneDelegateClassName - $(PRODUCT_MODULE_NAME).SceneDelegate - UISceneStoryboardFile - Main - - - - - UIApplicationSupportsIndirectInputEvents - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - - diff --git a/apps/oscortex_app/ios/Runner/Runner-Bridging-Header.h b/apps/oscortex_app/ios/Runner/Runner-Bridging-Header.h deleted file mode 100644 index 308a2a5..0000000 --- a/apps/oscortex_app/ios/Runner/Runner-Bridging-Header.h +++ /dev/null @@ -1 +0,0 @@ -#import "GeneratedPluginRegistrant.h" diff --git a/apps/oscortex_app/ios/Runner/SceneDelegate.swift b/apps/oscortex_app/ios/Runner/SceneDelegate.swift deleted file mode 100644 index b9ce8ea..0000000 --- a/apps/oscortex_app/ios/Runner/SceneDelegate.swift +++ /dev/null @@ -1,6 +0,0 @@ -import Flutter -import UIKit - -class SceneDelegate: FlutterSceneDelegate { - -} diff --git a/apps/oscortex_app/ios/RunnerTests/RunnerTests.swift b/apps/oscortex_app/ios/RunnerTests/RunnerTests.swift deleted file mode 100644 index 86a7c3b..0000000 --- a/apps/oscortex_app/ios/RunnerTests/RunnerTests.swift +++ /dev/null @@ -1,12 +0,0 @@ -import Flutter -import UIKit -import XCTest - -class RunnerTests: XCTestCase { - - func testExample() { - // If you add code to the Runner application, consider adding tests here. - // See https://developer.apple.com/documentation/xctest for more information about using XCTest. - } - -} diff --git a/apps/oscortex_app/linux/.gitignore b/apps/oscortex_app/linux/.gitignore deleted file mode 100644 index d3896c9..0000000 --- a/apps/oscortex_app/linux/.gitignore +++ /dev/null @@ -1 +0,0 @@ -flutter/ephemeral diff --git a/apps/oscortex_app/linux/CMakeLists.txt b/apps/oscortex_app/linux/CMakeLists.txt deleted file mode 100644 index 8c3beeb..0000000 --- a/apps/oscortex_app/linux/CMakeLists.txt +++ /dev/null @@ -1,128 +0,0 @@ -# Project-level configuration. -cmake_minimum_required(VERSION 3.13) -project(runner LANGUAGES CXX) - -# The name of the executable created for the application. Change this to change -# the on-disk name of your application. -set(BINARY_NAME "oscortex_app") -# The unique GTK application identifier for this application. See: -# https://wiki.gnome.org/HowDoI/ChooseApplicationID -set(APPLICATION_ID "com.example.oscortex_app") - -# Explicitly opt in to modern CMake behaviors to avoid warnings with recent -# versions of CMake. -cmake_policy(SET CMP0063 NEW) - -# Load bundled libraries from the lib/ directory relative to the binary. -set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") - -# Root filesystem for cross-building. -if(FLUTTER_TARGET_PLATFORM_SYSROOT) - set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) - set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) - set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) - set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) - set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) - set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) -endif() - -# Define build configuration options. -if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) - set(CMAKE_BUILD_TYPE "Debug" CACHE - STRING "Flutter build mode" FORCE) - set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS - "Debug" "Profile" "Release") -endif() - -# Compilation settings that should be applied to most targets. -# -# Be cautious about adding new options here, as plugins use this function by -# default. In most cases, you should add new options to specific targets instead -# of modifying this function. -function(APPLY_STANDARD_SETTINGS TARGET) - target_compile_features(${TARGET} PUBLIC cxx_std_14) - target_compile_options(${TARGET} PRIVATE -Wall -Werror) - target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") - target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") -endfunction() - -# Flutter library and tool build rules. -set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") -add_subdirectory(${FLUTTER_MANAGED_DIR}) - -# System-level dependencies. -find_package(PkgConfig REQUIRED) -pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) - -# Application build; see runner/CMakeLists.txt. -add_subdirectory("runner") - -# Run the Flutter tool portions of the build. This must not be removed. -add_dependencies(${BINARY_NAME} flutter_assemble) - -# Only the install-generated bundle's copy of the executable will launch -# correctly, since the resources must in the right relative locations. To avoid -# people trying to run the unbundled copy, put it in a subdirectory instead of -# the default top-level location. -set_target_properties(${BINARY_NAME} - PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" -) - - -# Generated plugin build rules, which manage building the plugins and adding -# them to the application. -include(flutter/generated_plugins.cmake) - - -# === Installation === -# By default, "installing" just makes a relocatable bundle in the build -# directory. -set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") -if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) - set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) -endif() - -# Start with a clean build bundle directory every time. -install(CODE " - file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") - " COMPONENT Runtime) - -set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") -set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") - -install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" - COMPONENT Runtime) - -install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" - COMPONENT Runtime) - -install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) - -foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) - install(FILES "${bundled_library}" - DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) -endforeach(bundled_library) - -# Copy the native assets provided by the build.dart from all packages. -set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") -install(DIRECTORY "${NATIVE_ASSETS_DIR}" - DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) - -# Fully re-copy the assets directory on each build to avoid having stale files -# from a previous install. -set(FLUTTER_ASSET_DIR_NAME "flutter_assets") -install(CODE " - file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") - " COMPONENT Runtime) -install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" - DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) - -# Install the AOT library on non-Debug builds only. -if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") - install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) -endif() diff --git a/apps/oscortex_app/linux/flutter/CMakeLists.txt b/apps/oscortex_app/linux/flutter/CMakeLists.txt deleted file mode 100644 index d5bd016..0000000 --- a/apps/oscortex_app/linux/flutter/CMakeLists.txt +++ /dev/null @@ -1,88 +0,0 @@ -# This file controls Flutter-level build steps. It should not be edited. -cmake_minimum_required(VERSION 3.10) - -set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") - -# Configuration provided via flutter tool. -include(${EPHEMERAL_DIR}/generated_config.cmake) - -# TODO: Move the rest of this into files in ephemeral. See -# https://github.com/flutter/flutter/issues/57146. - -# Serves the same purpose as list(TRANSFORM ... PREPEND ...), -# which isn't available in 3.10. -function(list_prepend LIST_NAME PREFIX) - set(NEW_LIST "") - foreach(element ${${LIST_NAME}}) - list(APPEND NEW_LIST "${PREFIX}${element}") - endforeach(element) - set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) -endfunction() - -# === Flutter Library === -# System-level dependencies. -find_package(PkgConfig REQUIRED) -pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) -pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) -pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) - -set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") - -# Published to parent scope for install step. -set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) -set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) -set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) -set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) - -list(APPEND FLUTTER_LIBRARY_HEADERS - "fl_basic_message_channel.h" - "fl_binary_codec.h" - "fl_binary_messenger.h" - "fl_dart_project.h" - "fl_engine.h" - "fl_json_message_codec.h" - "fl_json_method_codec.h" - "fl_message_codec.h" - "fl_method_call.h" - "fl_method_channel.h" - "fl_method_codec.h" - "fl_method_response.h" - "fl_plugin_registrar.h" - "fl_plugin_registry.h" - "fl_standard_message_codec.h" - "fl_standard_method_codec.h" - "fl_string_codec.h" - "fl_value.h" - "fl_view.h" - "flutter_linux.h" -) -list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") -add_library(flutter INTERFACE) -target_include_directories(flutter INTERFACE - "${EPHEMERAL_DIR}" -) -target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") -target_link_libraries(flutter INTERFACE - PkgConfig::GTK - PkgConfig::GLIB - PkgConfig::GIO -) -add_dependencies(flutter flutter_assemble) - -# === Flutter tool backend === -# _phony_ is a non-existent file to force this command to run every time, -# since currently there's no way to get a full input/output list from the -# flutter tool. -add_custom_command( - OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} - ${CMAKE_CURRENT_BINARY_DIR}/_phony_ - COMMAND ${CMAKE_COMMAND} -E env - ${FLUTTER_TOOL_ENVIRONMENT} - "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" - ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} - VERBATIM -) -add_custom_target(flutter_assemble DEPENDS - "${FLUTTER_LIBRARY}" - ${FLUTTER_LIBRARY_HEADERS} -) diff --git a/apps/oscortex_app/linux/flutter/generated_plugin_registrant.cc b/apps/oscortex_app/linux/flutter/generated_plugin_registrant.cc deleted file mode 100644 index e71a16d..0000000 --- a/apps/oscortex_app/linux/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,11 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - - -void fl_register_plugins(FlPluginRegistry* registry) { -} diff --git a/apps/oscortex_app/linux/flutter/generated_plugin_registrant.h b/apps/oscortex_app/linux/flutter/generated_plugin_registrant.h deleted file mode 100644 index e0f0a47..0000000 --- a/apps/oscortex_app/linux/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void fl_register_plugins(FlPluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/apps/oscortex_app/linux/flutter/generated_plugins.cmake b/apps/oscortex_app/linux/flutter/generated_plugins.cmake deleted file mode 100644 index 2e1de87..0000000 --- a/apps/oscortex_app/linux/flutter/generated_plugins.cmake +++ /dev/null @@ -1,23 +0,0 @@ -# -# Generated file, do not edit. -# - -list(APPEND FLUTTER_PLUGIN_LIST -) - -list(APPEND FLUTTER_FFI_PLUGIN_LIST -) - -set(PLUGIN_BUNDLED_LIBRARIES) - -foreach(plugin ${FLUTTER_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) - target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) - list(APPEND PLUGIN_BUNDLED_LIBRARIES $) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) -endforeach(plugin) - -foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) -endforeach(ffi_plugin) diff --git a/apps/oscortex_app/linux/runner/CMakeLists.txt b/apps/oscortex_app/linux/runner/CMakeLists.txt deleted file mode 100644 index e97dabc..0000000 --- a/apps/oscortex_app/linux/runner/CMakeLists.txt +++ /dev/null @@ -1,26 +0,0 @@ -cmake_minimum_required(VERSION 3.13) -project(runner LANGUAGES CXX) - -# Define the application target. To change its name, change BINARY_NAME in the -# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer -# work. -# -# Any new source files that you add to the application should be added here. -add_executable(${BINARY_NAME} - "main.cc" - "my_application.cc" - "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" -) - -# Apply the standard set of build settings. This can be removed for applications -# that need different build settings. -apply_standard_settings(${BINARY_NAME}) - -# Add preprocessor definitions for the application ID. -add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") - -# Add dependency libraries. Add any application-specific dependencies here. -target_link_libraries(${BINARY_NAME} PRIVATE flutter) -target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) - -target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") diff --git a/apps/oscortex_app/linux/runner/main.cc b/apps/oscortex_app/linux/runner/main.cc deleted file mode 100644 index e7c5c54..0000000 --- a/apps/oscortex_app/linux/runner/main.cc +++ /dev/null @@ -1,6 +0,0 @@ -#include "my_application.h" - -int main(int argc, char** argv) { - g_autoptr(MyApplication) app = my_application_new(); - return g_application_run(G_APPLICATION(app), argc, argv); -} diff --git a/apps/oscortex_app/linux/runner/my_application.cc b/apps/oscortex_app/linux/runner/my_application.cc deleted file mode 100644 index 6311b86..0000000 --- a/apps/oscortex_app/linux/runner/my_application.cc +++ /dev/null @@ -1,148 +0,0 @@ -#include "my_application.h" - -#include -#ifdef GDK_WINDOWING_X11 -#include -#endif - -#include "flutter/generated_plugin_registrant.h" - -struct _MyApplication { - GtkApplication parent_instance; - char** dart_entrypoint_arguments; -}; - -G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) - -// Called when first Flutter frame received. -static void first_frame_cb(MyApplication* self, FlView* view) { - gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view))); -} - -// Implements GApplication::activate. -static void my_application_activate(GApplication* application) { - MyApplication* self = MY_APPLICATION(application); - GtkWindow* window = - GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); - - // Use a header bar when running in GNOME as this is the common style used - // by applications and is the setup most users will be using (e.g. Ubuntu - // desktop). - // If running on X and not using GNOME then just use a traditional title bar - // in case the window manager does more exotic layout, e.g. tiling. - // If running on Wayland assume the header bar will work (may need changing - // if future cases occur). - gboolean use_header_bar = TRUE; -#ifdef GDK_WINDOWING_X11 - GdkScreen* screen = gtk_window_get_screen(window); - if (GDK_IS_X11_SCREEN(screen)) { - const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); - if (g_strcmp0(wm_name, "GNOME Shell") != 0) { - use_header_bar = FALSE; - } - } -#endif - if (use_header_bar) { - GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); - gtk_widget_show(GTK_WIDGET(header_bar)); - gtk_header_bar_set_title(header_bar, "oscortex_app"); - gtk_header_bar_set_show_close_button(header_bar, TRUE); - gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); - } else { - gtk_window_set_title(window, "oscortex_app"); - } - - gtk_window_set_default_size(window, 1280, 720); - - g_autoptr(FlDartProject) project = fl_dart_project_new(); - fl_dart_project_set_dart_entrypoint_arguments( - project, self->dart_entrypoint_arguments); - - FlView* view = fl_view_new(project); - GdkRGBA background_color; - // Background defaults to black, override it here if necessary, e.g. #00000000 - // for transparent. - gdk_rgba_parse(&background_color, "#000000"); - fl_view_set_background_color(view, &background_color); - gtk_widget_show(GTK_WIDGET(view)); - gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); - - // Show the window when Flutter renders. - // Requires the view to be realized so we can start rendering. - g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb), - self); - gtk_widget_realize(GTK_WIDGET(view)); - - fl_register_plugins(FL_PLUGIN_REGISTRY(view)); - - gtk_widget_grab_focus(GTK_WIDGET(view)); -} - -// Implements GApplication::local_command_line. -static gboolean my_application_local_command_line(GApplication* application, - gchar*** arguments, - int* exit_status) { - MyApplication* self = MY_APPLICATION(application); - // Strip out the first argument as it is the binary name. - self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); - - g_autoptr(GError) error = nullptr; - if (!g_application_register(application, nullptr, &error)) { - g_warning("Failed to register: %s", error->message); - *exit_status = 1; - return TRUE; - } - - g_application_activate(application); - *exit_status = 0; - - return TRUE; -} - -// Implements GApplication::startup. -static void my_application_startup(GApplication* application) { - // MyApplication* self = MY_APPLICATION(object); - - // Perform any actions required at application startup. - - G_APPLICATION_CLASS(my_application_parent_class)->startup(application); -} - -// Implements GApplication::shutdown. -static void my_application_shutdown(GApplication* application) { - // MyApplication* self = MY_APPLICATION(object); - - // Perform any actions required at application shutdown. - - G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); -} - -// Implements GObject::dispose. -static void my_application_dispose(GObject* object) { - MyApplication* self = MY_APPLICATION(object); - g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); - G_OBJECT_CLASS(my_application_parent_class)->dispose(object); -} - -static void my_application_class_init(MyApplicationClass* klass) { - G_APPLICATION_CLASS(klass)->activate = my_application_activate; - G_APPLICATION_CLASS(klass)->local_command_line = - my_application_local_command_line; - G_APPLICATION_CLASS(klass)->startup = my_application_startup; - G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; - G_OBJECT_CLASS(klass)->dispose = my_application_dispose; -} - -static void my_application_init(MyApplication* self) {} - -MyApplication* my_application_new() { - // Set the program name to the application ID, which helps various systems - // like GTK and desktop environments map this running application to its - // corresponding .desktop file. This ensures better integration by allowing - // the application to be recognized beyond its binary name. - g_set_prgname(APPLICATION_ID); - - return MY_APPLICATION(g_object_new(my_application_get_type(), - "application-id", APPLICATION_ID, "flags", - G_APPLICATION_NON_UNIQUE, nullptr)); -} diff --git a/apps/oscortex_app/linux/runner/my_application.h b/apps/oscortex_app/linux/runner/my_application.h deleted file mode 100644 index db16367..0000000 --- a/apps/oscortex_app/linux/runner/my_application.h +++ /dev/null @@ -1,21 +0,0 @@ -#ifndef FLUTTER_MY_APPLICATION_H_ -#define FLUTTER_MY_APPLICATION_H_ - -#include - -G_DECLARE_FINAL_TYPE(MyApplication, - my_application, - MY, - APPLICATION, - GtkApplication) - -/** - * my_application_new: - * - * Creates a new Flutter-based application. - * - * Returns: a new #MyApplication. - */ -MyApplication* my_application_new(); - -#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/apps/oscortex_app/macos/.gitignore b/apps/oscortex_app/macos/.gitignore deleted file mode 100644 index 746adbb..0000000 --- a/apps/oscortex_app/macos/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -# Flutter-related -**/Flutter/ephemeral/ -**/Pods/ - -# Xcode-related -**/dgph -**/xcuserdata/ diff --git a/apps/oscortex_app/macos/Flutter/Flutter-Debug.xcconfig b/apps/oscortex_app/macos/Flutter/Flutter-Debug.xcconfig deleted file mode 100644 index c2efd0b..0000000 --- a/apps/oscortex_app/macos/Flutter/Flutter-Debug.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/apps/oscortex_app/macos/Flutter/Flutter-Release.xcconfig b/apps/oscortex_app/macos/Flutter/Flutter-Release.xcconfig deleted file mode 100644 index c2efd0b..0000000 --- a/apps/oscortex_app/macos/Flutter/Flutter-Release.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/apps/oscortex_app/macos/Flutter/GeneratedPluginRegistrant.swift b/apps/oscortex_app/macos/Flutter/GeneratedPluginRegistrant.swift deleted file mode 100644 index cccf817..0000000 --- a/apps/oscortex_app/macos/Flutter/GeneratedPluginRegistrant.swift +++ /dev/null @@ -1,10 +0,0 @@ -// -// Generated file. Do not edit. -// - -import FlutterMacOS -import Foundation - - -func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { -} diff --git a/apps/oscortex_app/macos/Runner.xcodeproj/project.pbxproj b/apps/oscortex_app/macos/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index cc667c8..0000000 --- a/apps/oscortex_app/macos/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,705 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 54; - objects = { - -/* Begin PBXAggregateTarget section */ - 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { - isa = PBXAggregateTarget; - buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; - buildPhases = ( - 33CC111E2044C6BF0003C045 /* ShellScript */, - ); - dependencies = ( - ); - name = "Flutter Assemble"; - productName = FLX; - }; -/* End PBXAggregateTarget section */ - -/* Begin PBXBuildFile section */ - 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; - 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; - 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; - 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; - 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; - 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 33CC10E52044A3C60003C045 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 33CC10EC2044A3C60003C045; - remoteInfo = Runner; - }; - 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 33CC10E52044A3C60003C045 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 33CC111A2044C6BA0003C045; - remoteInfo = FLX; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 33CC110E2044A8840003C045 /* Bundle Framework */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Bundle Framework"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; - 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; - 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* oscortex_app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "oscortex_app.app"; sourceTree = BUILT_PRODUCTS_DIR; }; - 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; - 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; - 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; - 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; - 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; - 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; - 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; - 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; - 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; - 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 331C80D2294CF70F00263BE5 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 33CC10EA2044A3C60003C045 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 331C80D6294CF71000263BE5 /* RunnerTests */ = { - isa = PBXGroup; - children = ( - 331C80D7294CF71000263BE5 /* RunnerTests.swift */, - ); - path = RunnerTests; - sourceTree = ""; - }; - 33BA886A226E78AF003329D5 /* Configs */ = { - isa = PBXGroup; - children = ( - 33E5194F232828860026EE4D /* AppInfo.xcconfig */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, - ); - path = Configs; - sourceTree = ""; - }; - 33CC10E42044A3C60003C045 = { - isa = PBXGroup; - children = ( - 33FAB671232836740065AC1E /* Runner */, - 33CEB47122A05771004F2AC0 /* Flutter */, - 331C80D6294CF71000263BE5 /* RunnerTests */, - 33CC10EE2044A3C60003C045 /* Products */, - D73912EC22F37F3D000D13A0 /* Frameworks */, - ); - sourceTree = ""; - }; - 33CC10EE2044A3C60003C045 /* Products */ = { - isa = PBXGroup; - children = ( - 33CC10ED2044A3C60003C045 /* oscortex_app.app */, - 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - 33CC11242044D66E0003C045 /* Resources */ = { - isa = PBXGroup; - children = ( - 33CC10F22044A3C60003C045 /* Assets.xcassets */, - 33CC10F42044A3C60003C045 /* MainMenu.xib */, - 33CC10F72044A3C60003C045 /* Info.plist */, - ); - name = Resources; - path = ..; - sourceTree = ""; - }; - 33CEB47122A05771004F2AC0 /* Flutter */ = { - isa = PBXGroup; - children = ( - 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, - 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, - 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, - 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, - ); - path = Flutter; - sourceTree = ""; - }; - 33FAB671232836740065AC1E /* Runner */ = { - isa = PBXGroup; - children = ( - 33CC10F02044A3C60003C045 /* AppDelegate.swift */, - 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, - 33E51913231747F40026EE4D /* DebugProfile.entitlements */, - 33E51914231749380026EE4D /* Release.entitlements */, - 33CC11242044D66E0003C045 /* Resources */, - 33BA886A226E78AF003329D5 /* Configs */, - ); - path = Runner; - sourceTree = ""; - }; - D73912EC22F37F3D000D13A0 /* Frameworks */ = { - isa = PBXGroup; - children = ( - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 331C80D4294CF70F00263BE5 /* RunnerTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; - buildPhases = ( - 331C80D1294CF70F00263BE5 /* Sources */, - 331C80D2294CF70F00263BE5 /* Frameworks */, - 331C80D3294CF70F00263BE5 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 331C80DA294CF71000263BE5 /* PBXTargetDependency */, - ); - name = RunnerTests; - productName = RunnerTests; - productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 33CC10EC2044A3C60003C045 /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - 33CC10E92044A3C60003C045 /* Sources */, - 33CC10EA2044A3C60003C045 /* Frameworks */, - 33CC10EB2044A3C60003C045 /* Resources */, - 33CC110E2044A8840003C045 /* Bundle Framework */, - 3399D490228B24CF009A79C7 /* ShellScript */, - ); - buildRules = ( - ); - dependencies = ( - 33CC11202044C79F0003C045 /* PBXTargetDependency */, - ); - name = Runner; - productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* oscortex_app.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 33CC10E52044A3C60003C045 /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = YES; - LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1510; - ORGANIZATIONNAME = ""; - TargetAttributes = { - 331C80D4294CF70F00263BE5 = { - CreatedOnToolsVersion = 14.0; - TestTargetID = 33CC10EC2044A3C60003C045; - }; - 33CC10EC2044A3C60003C045 = { - CreatedOnToolsVersion = 9.2; - LastSwiftMigration = 1100; - ProvisioningStyle = Automatic; - SystemCapabilities = { - com.apple.Sandbox = { - enabled = 1; - }; - }; - }; - 33CC111A2044C6BA0003C045 = { - CreatedOnToolsVersion = 9.2; - ProvisioningStyle = Manual; - }; - }; - }; - buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 33CC10E42044A3C60003C045; - productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 33CC10EC2044A3C60003C045 /* Runner */, - 331C80D4294CF70F00263BE5 /* RunnerTests */, - 33CC111A2044C6BA0003C045 /* Flutter Assemble */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 331C80D3294CF70F00263BE5 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 33CC10EB2044A3C60003C045 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, - 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3399D490228B24CF009A79C7 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; - }; - 33CC111E2044C6BF0003C045 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - Flutter/ephemeral/FlutterInputs.xcfilelist, - ); - inputPaths = ( - Flutter/ephemeral/tripwire, - ); - outputFileListPaths = ( - Flutter/ephemeral/FlutterOutputs.xcfilelist, - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 331C80D1294CF70F00263BE5 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 33CC10E92044A3C60003C045 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, - 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, - 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 33CC10EC2044A3C60003C045 /* Runner */; - targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; - }; - 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; - targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { - isa = PBXVariantGroup; - children = ( - 33CC10F52044A3C60003C045 /* Base */, - ); - name = MainMenu.xib; - path = Runner; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 331C80DB294CF71000263BE5 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.oscortexApp.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/oscortex_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/oscortex_app"; - }; - name = Debug; - }; - 331C80DC294CF71000263BE5 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.oscortexApp.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/oscortex_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/oscortex_app"; - }; - name = Release; - }; - 331C80DD294CF71000263BE5 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.oscortexApp.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/oscortex_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/oscortex_app"; - }; - name = Profile; - }; - 338D0CE9231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEAD_CODE_STRIPPING = YES; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - }; - name = Profile; - }; - 338D0CEA231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; - }; - name = Profile; - }; - 338D0CEB231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Manual; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Profile; - }; - 33CC10F92044A3C60003C045 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEAD_CODE_STRIPPING = YES; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = macosx; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - 33CC10FA2044A3C60003C045 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEAD_CODE_STRIPPING = YES; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.15; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - }; - name = Release; - }; - 33CC10FC2044A3C60003C045 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - }; - name = Debug; - }; - 33CC10FD2044A3C60003C045 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; - }; - name = Release; - }; - 33CC111C2044C6BA0003C045 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Manual; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Debug; - }; - 33CC111D2044C6BA0003C045 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 331C80DB294CF71000263BE5 /* Debug */, - 331C80DC294CF71000263BE5 /* Release */, - 331C80DD294CF71000263BE5 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC10F92044A3C60003C045 /* Debug */, - 33CC10FA2044A3C60003C045 /* Release */, - 338D0CE9231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC10FC2044A3C60003C045 /* Debug */, - 33CC10FD2044A3C60003C045 /* Release */, - 338D0CEA231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC111C2044C6BA0003C045 /* Debug */, - 33CC111D2044C6BA0003C045 /* Release */, - 338D0CEB231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 33CC10E52044A3C60003C045 /* Project object */; -} diff --git a/apps/oscortex_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/apps/oscortex_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d9810..0000000 --- a/apps/oscortex_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/apps/oscortex_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/apps/oscortex_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index a0457b0..0000000 --- a/apps/oscortex_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,99 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/oscortex_app/macos/Runner.xcworkspace/contents.xcworkspacedata b/apps/oscortex_app/macos/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 1d526a1..0000000 --- a/apps/oscortex_app/macos/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/apps/oscortex_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/apps/oscortex_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d9810..0000000 --- a/apps/oscortex_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/apps/oscortex_app/macos/Runner/AppDelegate.swift b/apps/oscortex_app/macos/Runner/AppDelegate.swift deleted file mode 100644 index b3c1761..0000000 --- a/apps/oscortex_app/macos/Runner/AppDelegate.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Cocoa -import FlutterMacOS - -@main -class AppDelegate: FlutterAppDelegate { - override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { - return true - } - - override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { - return true - } -} diff --git a/apps/oscortex_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/apps/oscortex_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index a2ec33f..0000000 --- a/apps/oscortex_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "images" : [ - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_16.png", - "scale" : "1x" - }, - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "2x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "1x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_64.png", - "scale" : "2x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_128.png", - "scale" : "1x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "2x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "1x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "2x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "1x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_1024.png", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/apps/oscortex_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/apps/oscortex_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png deleted file mode 100644 index 82b6f9d..0000000 Binary files a/apps/oscortex_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png and /dev/null differ diff --git a/apps/oscortex_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/apps/oscortex_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png deleted file mode 100644 index 13b35eb..0000000 Binary files a/apps/oscortex_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png and /dev/null differ diff --git a/apps/oscortex_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/apps/oscortex_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png deleted file mode 100644 index 0a3f5fa..0000000 Binary files a/apps/oscortex_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png and /dev/null differ diff --git a/apps/oscortex_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/apps/oscortex_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png deleted file mode 100644 index bdb5722..0000000 Binary files a/apps/oscortex_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png and /dev/null differ diff --git a/apps/oscortex_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/apps/oscortex_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png deleted file mode 100644 index f083318..0000000 Binary files a/apps/oscortex_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png and /dev/null differ diff --git a/apps/oscortex_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/apps/oscortex_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png deleted file mode 100644 index 326c0e7..0000000 Binary files a/apps/oscortex_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png and /dev/null differ diff --git a/apps/oscortex_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/apps/oscortex_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png deleted file mode 100644 index 2f1632c..0000000 Binary files a/apps/oscortex_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png and /dev/null differ diff --git a/apps/oscortex_app/macos/Runner/Base.lproj/MainMenu.xib b/apps/oscortex_app/macos/Runner/Base.lproj/MainMenu.xib deleted file mode 100644 index 80e867a..0000000 --- a/apps/oscortex_app/macos/Runner/Base.lproj/MainMenu.xib +++ /dev/null @@ -1,343 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/oscortex_app/macos/Runner/Configs/AppInfo.xcconfig b/apps/oscortex_app/macos/Runner/Configs/AppInfo.xcconfig deleted file mode 100644 index 3e49e0f..0000000 --- a/apps/oscortex_app/macos/Runner/Configs/AppInfo.xcconfig +++ /dev/null @@ -1,14 +0,0 @@ -// Application-level settings for the Runner target. -// -// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the -// future. If not, the values below would default to using the project name when this becomes a -// 'flutter create' template. - -// The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = oscortex_app - -// The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = com.example.oscortexApp - -// The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2026 com.example. All rights reserved. diff --git a/apps/oscortex_app/macos/Runner/Configs/Debug.xcconfig b/apps/oscortex_app/macos/Runner/Configs/Debug.xcconfig deleted file mode 100644 index 36b0fd9..0000000 --- a/apps/oscortex_app/macos/Runner/Configs/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Debug.xcconfig" -#include "Warnings.xcconfig" diff --git a/apps/oscortex_app/macos/Runner/Configs/Release.xcconfig b/apps/oscortex_app/macos/Runner/Configs/Release.xcconfig deleted file mode 100644 index dff4f49..0000000 --- a/apps/oscortex_app/macos/Runner/Configs/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Release.xcconfig" -#include "Warnings.xcconfig" diff --git a/apps/oscortex_app/macos/Runner/Configs/Warnings.xcconfig b/apps/oscortex_app/macos/Runner/Configs/Warnings.xcconfig deleted file mode 100644 index 42bcbf4..0000000 --- a/apps/oscortex_app/macos/Runner/Configs/Warnings.xcconfig +++ /dev/null @@ -1,13 +0,0 @@ -WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings -GCC_WARN_UNDECLARED_SELECTOR = YES -CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES -CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE -CLANG_WARN__DUPLICATE_METHOD_MATCH = YES -CLANG_WARN_PRAGMA_PACK = YES -CLANG_WARN_STRICT_PROTOTYPES = YES -CLANG_WARN_COMMA = YES -GCC_WARN_STRICT_SELECTOR_MATCH = YES -CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES -CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES -GCC_WARN_SHADOW = YES -CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/apps/oscortex_app/macos/Runner/DebugProfile.entitlements b/apps/oscortex_app/macos/Runner/DebugProfile.entitlements deleted file mode 100644 index dddb8a3..0000000 --- a/apps/oscortex_app/macos/Runner/DebugProfile.entitlements +++ /dev/null @@ -1,12 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.cs.allow-jit - - com.apple.security.network.server - - - diff --git a/apps/oscortex_app/macos/Runner/Info.plist b/apps/oscortex_app/macos/Runner/Info.plist deleted file mode 100644 index 4789daa..0000000 --- a/apps/oscortex_app/macos/Runner/Info.plist +++ /dev/null @@ -1,32 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIconFile - - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSMinimumSystemVersion - $(MACOSX_DEPLOYMENT_TARGET) - NSHumanReadableCopyright - $(PRODUCT_COPYRIGHT) - NSMainNibFile - MainMenu - NSPrincipalClass - NSApplication - - diff --git a/apps/oscortex_app/macos/Runner/MainFlutterWindow.swift b/apps/oscortex_app/macos/Runner/MainFlutterWindow.swift deleted file mode 100644 index 3cc05eb..0000000 --- a/apps/oscortex_app/macos/Runner/MainFlutterWindow.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Cocoa -import FlutterMacOS - -class MainFlutterWindow: NSWindow { - override func awakeFromNib() { - let flutterViewController = FlutterViewController() - let windowFrame = self.frame - self.contentViewController = flutterViewController - self.setFrame(windowFrame, display: true) - - RegisterGeneratedPlugins(registry: flutterViewController) - - super.awakeFromNib() - } -} diff --git a/apps/oscortex_app/macos/Runner/Release.entitlements b/apps/oscortex_app/macos/Runner/Release.entitlements deleted file mode 100644 index 852fa1a..0000000 --- a/apps/oscortex_app/macos/Runner/Release.entitlements +++ /dev/null @@ -1,8 +0,0 @@ - - - - - com.apple.security.app-sandbox - - - diff --git a/apps/oscortex_app/macos/RunnerTests/RunnerTests.swift b/apps/oscortex_app/macos/RunnerTests/RunnerTests.swift deleted file mode 100644 index 61f3bd1..0000000 --- a/apps/oscortex_app/macos/RunnerTests/RunnerTests.swift +++ /dev/null @@ -1,12 +0,0 @@ -import Cocoa -import FlutterMacOS -import XCTest - -class RunnerTests: XCTestCase { - - func testExample() { - // If you add code to the Runner application, consider adding tests here. - // See https://developer.apple.com/documentation/xctest for more information about using XCTest. - } - -} diff --git a/apps/oscortex_web_link/.gitignore b/apps/oscortex_web_link/.gitignore new file mode 100644 index 0000000..54c072e --- /dev/null +++ b/apps/oscortex_web_link/.gitignore @@ -0,0 +1,3 @@ + +# OSCortex build artifacts +build/osx/ diff --git a/apps/oscortex_web_link/lib/main.dart b/apps/oscortex_web_link/lib/main.dart index 7e01229..4e081e5 100644 --- a/apps/oscortex_web_link/lib/main.dart +++ b/apps/oscortex_web_link/lib/main.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:oscortex_ui/oscortex_ui.dart'; +import 'package:oscortex_webview/oscortex_webview.dart'; void main() { runApp(const WebLinkApp()); @@ -18,6 +19,11 @@ class WebLinkApp extends StatelessWidget { } } +/// Web Link — the OSCortex system browser. Chrome (address/search bar, nav, +/// progress) wrapping the native [OscWebView], whose page is rendered by the +/// userspace web-engine service into an OSCortex compositor surface beneath this +/// scene. Until that engine service binds, navigation is inert and the web region +/// shows a placeholder (the controller tolerates a missing engine). class WebLinkHome extends StatefulWidget { const WebLinkHome({super.key}); @@ -26,364 +32,261 @@ class WebLinkHome extends StatefulWidget { } class _WebLinkHomeState extends State { - final _controller = - TextEditingController(text: 'https://dccortec.com/docs/arch'); + static const String _homeUrl = 'https://dotcorr.com/'; + + final OscWebViewController _web = OscWebViewController(1); + final TextEditingController _address = TextEditingController(text: _homeUrl); + final FocusNode _addressFocus = FocusNode(); + + int _progress = 0; + bool _loading = false; + bool _canBack = false; + bool _canForward = false; + bool _engineReady = false; // a render surface exists + bool _createRequested = false; + + @override + void initState() { + super.initState(); + _web.onUrlChanged = (String url) { + if (!_addressFocus.hasFocus) _address.text = url; + }; + // onTitleChanged is wired when tabs land (which display the page title). + _web.onProgress = (int p) { + setState(() => _progress = p); + }; + _web.onLoadStarted = (String _) { + setState(() => _loading = true); + }; + _web.onLoadFinished = (String _) { + setState(() { + _loading = false; + _progress = 100; + }); + }; + _web.onLoadError = (int _, String _, String _) { + setState(() => _loading = false); + }; + _web.onNavState = (bool back, bool fwd) { + setState(() { + _canBack = back; + _canForward = fwd; + }); + }; + } @override void dispose() { - _controller.dispose(); + _web.dispose(); + _address.dispose(); + _addressFocus.dispose(); super.dispose(); } + /// Turn raw omnibox input into a URL: keep explicit schemes, assume https for + /// bare hosts, otherwise treat it as a web search (the "search" wrapper). + String _toUrl(String input) { + final String s = input.trim(); + if (s.isEmpty) return s; + if (RegExp(r'^[a-zA-Z][a-zA-Z0-9+.\-]*://').hasMatch(s)) return s; + if (!s.contains(' ') && RegExp(r'^[^\s/]+\.[^\s/]+').hasMatch(s)) { + return 'https://$s'; + } + return 'https://duckduckgo.com/?q=${Uri.encodeQueryComponent(s)}'; + } + + void _navigate() { + final String url = _toUrl(_address.text); + if (url.isEmpty) return; + _addressFocus.unfocus(); + setState(() { + _loading = true; + _progress = 0; + }); + _web.loadUrl(url); + } + + Future _ensureSurface(Size size, double dpr) async { + if (_createRequested || size.isEmpty) return; + _createRequested = true; + final int sid = await _web.create( + width: (size.width * dpr).round(), + height: (size.height * dpr).round(), + dpr: dpr, + initialUrl: _toUrl(_address.text), + ); + if (mounted && sid >= 0) setState(() => _engineReady = true); + } + @override Widget build(BuildContext context) { + final double dpr = MediaQuery.of(context).devicePixelRatio; return Scaffold( backgroundColor: OscColors.bodyBg, - body: Stack( - children: [ - // ── Subtle emerald radial gradient background - Positioned.fill( - child: DecoratedBox( - decoration: BoxDecoration( - gradient: RadialGradient( - center: const Alignment(-0.6, -0.8), - radius: 1.4, - colors: [ - OscColors.green.withValues(alpha: 0.045), - OscColors.bodyBg.withValues(alpha: 0.0), - ], - stops: const [0.0, 0.7], - ), + body: SafeArea( + child: Column( + children: [ + _toolbar(), + _progressBar(), + Expanded( + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints c) { + final Size size = Size(c.maxWidth, c.maxHeight); + WidgetsBinding.instance.addPostFrameCallback( + (_) => _ensureSurface(size, dpr)); + return _engineReady + ? OscWebView(controller: _web) + : _placeholder(); + }, ), ), + ], + ), + ), + ); + } + + Widget _toolbar() { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: const BoxDecoration( + color: OscColors.canvasBg, + border: Border(bottom: BorderSide(color: OscColors.border)), + ), + child: Row( + children: [ + OscIconButton( + icon: Icons.arrow_back_rounded, + tooltip: 'Back', + onPressed: _canBack ? _web.goBack : null, ), - // ── Second subtle gradient from bottom-right - Positioned.fill( - child: DecoratedBox( - decoration: BoxDecoration( - gradient: RadialGradient( - center: const Alignment(0.9, 1.0), - radius: 1.2, - colors: [ - OscColors.violet.withValues(alpha: 0.025), - Colors.transparent, - ], - stops: const [0.0, 0.6], - ), - ), - ), + const SizedBox(width: 4), + OscIconButton( + icon: Icons.arrow_forward_rounded, + tooltip: 'Forward', + onPressed: _canForward ? _web.goForward : null, ), - // ── Main content - SafeArea( - child: SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 24), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // ── Title - Text( - 'Web Link', - style: OscTypography.heading1.copyWith( - color: OscColors.textPrimary, - letterSpacing: -0.5, - height: 1.1, - ), - ), - const SizedBox(height: 6), - Row( - children: [ - Container( - width: 6, - height: 6, - decoration: const BoxDecoration( - color: OscColors.green, - shape: BoxShape.circle, - ), - ), - const SizedBox(width: 8), - Text( - 'Knowledge source & media gateway', - style: OscTypography.caption.copyWith( - fontSize: 14, - color: OscColors.textMuted, - fontWeight: FontWeight.w400, - letterSpacing: 0.2, - ), - ), - ], - ), - const SizedBox(height: 28), - - // ── URL Input - TextField( - controller: _controller, - style: OscTypography.monoBody.copyWith( - color: OscColors.textPrimary, - fontSize: 14, - ), - decoration: const InputDecoration( - labelText: 'Link target', - prefixIcon: Icon(Icons.link_rounded), - ), - ), - const SizedBox(height: 16), - - // ── Open Link button - FilledButton.icon( - onPressed: () { - final url = _controller.text.trim(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Row( - children: [ - const Icon(Icons.open_in_new_rounded, - color: OscColors.green, size: 16), - const SizedBox(width: 10), - Expanded( - child: Text( - 'Queued link: $url', - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ), - ); - }, - icon: const Icon(Icons.open_in_new_rounded, size: 18), - label: const Text('Open Link'), - ), - const SizedBox(height: 32), - - // ── Knowledge Source Engine Card - _buildKnowledgeSourceCard(), - const SizedBox(height: 20), - - // ── Media Player Card - _buildMediaPlayerCard(), - ], - ), - ), + const SizedBox(width: 4), + OscIconButton( + icon: _loading ? Icons.close_rounded : Icons.refresh_rounded, + tooltip: _loading ? 'Stop' : 'Reload', + onPressed: _loading ? _web.stopLoading : _web.reload, ), + const SizedBox(width: 10), + Expanded(child: _addressBar()), ], ), ); } - Widget _buildKnowledgeSourceCard() { - return OscCard( - padding: EdgeInsets.zero, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // ── Card header bar - Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14), - decoration: const BoxDecoration( - border: Border( - bottom: BorderSide(color: OscColors.border, width: 1), - ), - ), - child: Row( - children: [ - OscTag.green(label: 'KNOWLEDGE SOURCE ENGINE'), - const Spacer(), - Container( - width: 8, - height: 8, - decoration: BoxDecoration( - color: OscColors.green, - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: OscColors.green.withValues(alpha: 0.5), - blurRadius: 6, - spreadRadius: 1, - ), - ], - ), - ), - const SizedBox(width: 6), - Text( - 'LIVE', - style: OscTypography.monoLabel.copyWith( - color: OscColors.green, - letterSpacing: 1.0, - ), - ), - ], - ), - ), - - // ── Card content - Padding( - padding: const EdgeInsets.all(18), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // URL preview - Row( - children: [ - const Icon(Icons.language_rounded, - color: OscColors.skyBlue, size: 14), - const SizedBox(width: 8), - Expanded( - child: Text( - _controller.text, - style: OscTypography.monoBody.copyWith( - fontSize: 12, - color: OscColors.skyBlue, - ), - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - const SizedBox(height: 16), - - // Page title - Text( - 'Operating Systems Architecture Evolution', - style: OscTypography.heading3.copyWith( - fontSize: 18, - fontWeight: FontWeight.w700, - color: OscColors.textPrimary, - height: 1.3, - ), - ), - const SizedBox(height: 10), - - // Description - Text( - 'Comprehensive guide covering microkernel design principles, ' - 'memory-safe driver isolation, capability-based security models, ' - 'and the convergence of embedded real-time constraints with ' - 'modern desktop compositing pipelines. Explores how OSCortex ' - 'synthesizes these paradigms into a unified bare-metal runtime.', - style: OscTypography.body.copyWith( - color: OscColors.textMuted, - height: 1.65, - fontWeight: FontWeight.w400, - ), - ), - const SizedBox(height: 18), - - // Meta tags row - Row( - children: [ - _metaTag(Icons.schedule_rounded, '12 min read'), - const SizedBox(width: 10), - _metaTag(Icons.bookmark_outline_rounded, 'Saved'), - const Spacer(), - OscTag.green(label: 'Architecture'), - ], - ), - ], - ), - ), - ], + Widget _addressBar() { + return TextField( + controller: _address, + focusNode: _addressFocus, + textInputAction: TextInputAction.go, + onSubmitted: (_) => _navigate(), + style: OscTypography.monoBody.copyWith( + color: OscColors.textPrimary, + fontSize: 13, ), + decoration: InputDecoration( + isDense: true, + contentPadding: + const EdgeInsets.symmetric(horizontal: 14, vertical: 11), + hintText: 'Search or enter address', + prefixIcon: const Icon(Icons.travel_explore_rounded, size: 16), + suffixIcon: IconButton( + icon: const Icon(Icons.subdirectory_arrow_left_rounded, size: 16), + color: OscColors.textMuted, + tooltip: 'Go', + onPressed: _navigate, + ), + filled: true, + fillColor: OscColors.surface, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: OscColors.border), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: OscColors.border), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: OscColors.green), + ), + ), + ); + } + + Widget _progressBar() { + // A 2px accent line while loading; invisible otherwise (keeps layout stable). + return SizedBox( + height: 2, + child: (_loading && _progress > 0 && _progress < 100) + ? LinearProgressIndicator( + value: _progress / 100.0, + minHeight: 2, + backgroundColor: Colors.transparent, + valueColor: + const AlwaysStoppedAnimation(OscColors.green), + ) + : const SizedBox.shrink(), ); } - Widget _buildMediaPlayerCard() { - return OscCard( - padding: EdgeInsets.zero, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // ── Header bar - Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14), - decoration: const BoxDecoration( - border: Border( - bottom: BorderSide(color: OscColors.border, width: 1), + Widget _placeholder() { + // Shown until the web-engine service binds and a render surface exists. + return DecoratedBox( + decoration: BoxDecoration( + gradient: RadialGradient( + center: const Alignment(-0.4, -0.7), + radius: 1.3, + colors: [ + OscColors.green.withValues(alpha: 0.05), + OscColors.bodyBg.withValues(alpha: 0.0), + ], + stops: const [0.0, 0.7], + ), + ), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.public_rounded, size: 44, color: OscColors.textDim), + const SizedBox(height: 16), + Text( + 'Web Link', + style: OscTypography.heading2.copyWith( + color: OscColors.textPrimary, + letterSpacing: -0.4, ), ), - child: Row( - children: [ - OscTag.violet(label: 'NOW PLAYING'), - const Spacer(), - Icon(Icons.volume_up_rounded, - size: 16, color: OscColors.violetLight.withValues(alpha: 0.6)), - ], - ), - ), - - // ── Body - Padding( - padding: const EdgeInsets.all(18), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Track info - Text( - 'Ambient Waves', - style: OscTypography.heading3.copyWith( - fontSize: 17, - fontWeight: FontWeight.w700, - color: OscColors.textPrimary, - height: 1.2, + const SizedBox(height: 8), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 6, + height: 6, + decoration: const BoxDecoration( + color: OscColors.amber, + shape: BoxShape.circle, ), ), - const SizedBox(height: 4), + const SizedBox(width: 8), Text( - 'Cortex Generative Radio', - style: OscTypography.body.copyWith( + 'web engine starting…', + style: OscTypography.caption.copyWith( color: OscColors.textMuted, - fontWeight: FontWeight.w400, - ), - ), - const SizedBox(height: 20), - - // Waveform visualiser - const Center( - child: OscWaveform( - barCount: 8, - height: 48, - barWidth: 6, - barGap: 3, + letterSpacing: 0.2, ), ), - const SizedBox(height: 20), - - // Tags - Row( - children: [ - OscTag.violet( - label: 'Local Sync', - icon: Icons.sync_rounded, - ), - const SizedBox(width: 10), - OscTag.violet( - label: 'Spatial Audio Active', - icon: Icons.spatial_audio_off_rounded, - ), - ], - ), ], ), - ), - ], - ), - ); - } - - Widget _metaTag(IconData icon, String label) { - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(icon, size: 13, color: OscColors.textMuted), - const SizedBox(width: 5), - Text( - label, - style: OscTypography.caption.copyWith( - fontWeight: FontWeight.w500, - ), + ], ), - ], + ), ); } } diff --git a/apps/oscortex_web_link/pubspec.yaml b/apps/oscortex_web_link/pubspec.yaml index 9910524..5e2fae1 100644 --- a/apps/oscortex_web_link/pubspec.yaml +++ b/apps/oscortex_web_link/pubspec.yaml @@ -11,6 +11,8 @@ dependencies: sdk: flutter oscortex_ui: path: ../../packages/oscortex_ui + oscortex_webview: + path: ../../packages/oscortex_webview dev_dependencies: flutter_lints: ^6.0.0 diff --git a/dev-tools/README.md b/dev-tools/README.md new file mode 100644 index 0000000..a1df506 --- /dev/null +++ b/dev-tools/README.md @@ -0,0 +1,113 @@ +# OSCortex dev-tools + +Tooling for **contributors** working on the OSCortex low level (kernel, drivers, +boot, engine bring-up). These are the harnesses and recovery scripts used to +diagnose and verify the boot/render/input stability of both architectures. + +> App-developer tooling (compiling `.osx` apps, the cross-arch emulator/preview) +> lives separately — see the `osx` CLI package. This folder is for **OS/kernel +> contributors**, not app developers. + +All scripts auto-detect the repo root (via `git rev-parse --show-toplevel`) and +take overridable env vars — no hard-coded paths. Run them from anywhere in the repo. + +## Layout + +| Path | Purpose | +|---|---| +| `test/freeze-repro.py` | aarch64 `.kernel` boot (HVF) → launch app → sweep → assert `present_callback` keeps advancing (the post-app-launch freeze regression test). Env: `OSCORTEX_KERNEL`. | +| `test/freeze-repro-x86.py` | x86_64 ISO boot (TCG, UEFI/OVMF) → render + launch + no-freeze. Env: `OSCORTEX_ISO`. | +| `test/input-test.py` | Boots with UTM-exact devices (virtio-vga + qemu-xhci + usb-tablet) + injects QMP pointer events → verifies the kernel binds + processes the pointer. | +| `build/restore-arm64.sh` | **Recover the arm64 build** after an x86 build clobbers the shared `initramfs/` (see "Build fragility" below). | +| `build/regen-x86-snapshots.sh` | Regenerate **product-mode** x86 AOT snapshots (shell + apps) from the AOT dills using the matching `gen_snapshot`. | + +All scripts take env overrides (e.g. `OSCORTEX_REPO`, `OSCORTEX_KERNEL`, `OSCORTEX_ISO`, +`OSCX_ENGINE_CONTAINER`) and default to repo-relative / `~/OSCortex-run` paths. For an +arm64 **ISO** (UEFI) freeze test, run `freeze-repro.py` against a kernel built into a +Limine ISO, or boot the ISO with the `freeze-repro-x86.py` QEMU pattern adapted to +`-M virt,accel=hvf -bios -cdrom `. + +## Quick start + +```sh +# Regression: does the freeze stay fixed on aarch64? +python3 dev-tools/test/freeze-repro.py + +# Boot the x86 ISO headless and check it renders + no freeze +OSCORTEX_ISO=/path/to/oscortex-x86_64.iso python3 dev-tools/test/freeze-repro-x86.py +``` + +## Hard-won knowledge (read before debugging boot/input) + +**Two real-hardware boot hangs that QEMU's default devices hid:** +- **Legacy PIT calibration** (x86): TSC/APIC calibration spun forever on the i8254 + PIT/speaker gate, which Apple firmware (and some UEFI) doesn't wire. Fixed by + CPUID-derived TSC freq + bounded PIT spins (`kernel/src/arch/x86_64/apic.rs`). + Reproduce the *path* with QEMU `-machine q35,pit=off`. +- **Legacy i8042 PS/2 drain** (x86, UTM): `ps2::init` flushed the i8042 with + unbounded `while (status & OBF)` loops; an absent controller reads `0xFF` (OBF + always set) → infinite spin → hang at `cortex::drivers`. Fixed with a 0xFF guard + + bounded drains (`kernel/src/drivers/ps2.rs`). QEMU's own q35 i8042 clears OBF, + hiding it headless — only UTM/real-HW reproduce. + +**On-screen boot phases:** the boot splash shows `cortex::` (set by +`bootscreen::set_phase` at each `shared_init_and_run` step). When a boot wedges on +hardware QEMU can't reproduce, the frozen splash names the last stage reached — +your fastest localizer when there's no serial. + +**Input device matrix (what the kernel binds):** +- aarch64: **virtio-input** (`virtio-tablet-device`/`virtio-keyboard-device`) on the + `ramfb` display. USB is gated on the limine-boot ISO (QEMU-11+HVF xHCI + `assert(isv)` crash). NOTE: adding virtio-input alongside UTM's `virtio-ramfb` + (virtio-GPU) display breaks the display — use plain `ramfb`. +- x86_64: **USB** via xHCI. Boot keyboard/mouse + **usb-tablet** (absolute, the + UTM/QEMU default pointer — protocol 0, parsed via the HID report descriptor). + PS/2 is skipped when no i8042 is present. + +**Serial:** the default kernel log level is `Error` (fast boot). Raise to `Info` in +`kernel/src/logger.rs` for deep probe visibility. UTM VMs have no serial port by +default — add one (or use the on-screen boot phases) to debug on-device. + +## On-screen input HUD (`--features input-hud`) + +When a VM/board shows **no cursor or no response to input** and there's no serial, +build the kernel with `--features input-hud`. A compact panel is drawn over every +frame (bottom-left): + +``` +INPUT src=virtio p=142 k=0 s=3 ← bound device + pointer/key/scroll event counts +xy=640,360 btn=1 key=0x00- ← last pointer pos / buttons / last key +``` + +Read it like this: +- **`src=none` + red border** → no input device bound. The kernel never found a + pointer/keyboard (wrong VM device set — see the device matrix above). +- **`src=…` + cyan border but `p=` never climbs** as you move the mouse → a device + bound but events aren't arriving (IRQ not wired, queue not kicked, descriptor + mis-parse). The device is the suspect, not the cursor draw. +- **`p=` climbs but `xy=` is pinned at an edge / off-screen** → events arrive but the + coordinates are scaled/clamped wrong (e.g. absolute-axis range mismatch). +- **`btn=` flips on click, `key=0x..` changes on keypress** → input is fully working. + +The telemetry counters themselves (`wm::input_counts`, `wm::input_source`) are always +compiled in (a few relaxed atomics); only the on-screen drawing is gated by the +feature, so **release ISOs are unchanged and pay zero cost**. Build a debug image: + +```sh +# x86_64 debug ISO with the HUD +X86_AOT=1 SKIP_CORE_APPS=1 KERNEL_FEATURES=input-hud bash scripts/build-iso.sh +# aarch64 .kernel with the HUD +cargo +nightly build --release --target aarch64-unknown-none -p oscortex-kernel \ + --no-default-features --features arch-aarch64,limine-boot,input-hud \ + -Z build-std=core,compiler_builtins,alloc -Z build-std-features=compiler-builtins-mem +``` + +## Build fragility (important) + +The aarch64 and x86 builds **share the `initramfs/` directory** and clobber each +other's staged artifacts (the x86 `build-iso.sh` strips `libapp.so`; the arm build +stages its own engine/snapshots). After building one arch, **re-stage before +building the other**, or use `build/restore-arm64.sh` to recover the aarch64 tree. +The arch-specific AOT snapshots are backed up under `apps/*/build/oscortex/` +(`libapp-arm64.so` / the x86 `libapp.so`). A proper fix (separate per-arch staging +dirs) is a tracked cleanup item. diff --git a/dev-tools/build/regen-x86-snapshots.sh b/dev-tools/build/regen-x86-snapshots.sh new file mode 100755 index 0000000..034f39f --- /dev/null +++ b/dev-tools/build/regen-x86-snapshots.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# Regenerate PRODUCT-mode x86_64 AOT snapshots (shell + bundled apps) from the +# prebuilt AOT dills, using a gen_snapshot that MATCHES the shipped release x64 +# engine. Needed when the prebuilt x86 libapp.so files are the wrong VM mode +# (the Dart VM rejects a 'release' snapshot under a 'product' engine). +# +# Prereqs: the `oscx-engine` build container with a built product gen_snapshot at +# out/oscortex_release_x64/gen_snapshot (build it with +# `ninja -C out/oscortex_release_x64 gen_snapshot`), and the per-app AOT dills at +# apps//build/app_aot.dill. +# +# GOTCHA: this gen_snapshot rejects `--dedup_instructions` as an unknown VM flag — +# do not pass it. +# +# Usage: dev-tools/build/regen-x86-snapshots.sh +set -euo pipefail + +ROOT="$(git -C "$(dirname "${BASH_SOURCE[0]}")" rev-parse --show-toplevel)" +CONTAINER="${OSCX_ENGINE_CONTAINER:-oscx-engine}" +GEN="${X64_GEN_SNAPSHOT:-/work/engine/engine/src/out/oscortex_release_x64/gen_snapshot}" + +regen() { # + local app="$1" + local dill="$ROOT/apps/$app/build/app_aot.dill" + [ -f "$dill" ] || { echo " MISSING dill: $dill" >&2; return 1; } + echo "[regen-x86] $app: app_aot.dill → product-mode libapp.so" + docker cp "$dill" "$CONTAINER:/work/${app}_aot.dill" + docker exec "$CONTAINER" bash -lc \ + "$GEN --deterministic --snapshot_kind=app-aot-elf --elf=/work/${app}_x64.so --strip /work/${app}_aot.dill" 2>&1 | tail -1 + docker cp "$CONTAINER:/work/${app}_x64.so" "$ROOT/apps/$app/build/oscortex/libapp.so" + echo " -> $(file "$ROOT/apps/$app/build/oscortex/libapp.so" | grep -oE 'x86-64') $(stat -f %z "$ROOT/apps/$app/build/oscortex/libapp.so" 2>/dev/null || stat -c %s "$ROOT/apps/$app/build/oscortex/libapp.so")b" +} + +regen oscortex_app +regen oscortex_files +regen oscortex_canvas +regen oscortex_web_link +echo "[regen-x86] done. Stage with: X86_AOT=1 SKIP_CORE_APPS=1 scripts/build-iso.sh" diff --git a/dev-tools/build/restore-arm64.sh b/dev-tools/build/restore-arm64.sh new file mode 100755 index 0000000..fe3fdbf --- /dev/null +++ b/dev-tools/build/restore-arm64.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# Recover the aarch64 build tree after an x86 build clobbered the shared +# initramfs/ (the build-fragility issue — see dev-tools/README.md). Re-stages the +# arm64 shell + app AOT snapshots, the arm64 engine, the arm64 embedder, drops the +# x86 JIT kernel_blob, and rebuilds the arm64 kernel. +# +# Prereqs: the `oscx-engine` build container running (holds the arm64 engine), and +# the prebuilt arm64 AOT snapshots under apps/*/build/oscortex/libapp-arm64.so. +# +# Usage: dev-tools/build/restore-arm64.sh +set -euo pipefail + +ROOT="$(git -C "$(dirname "${BASH_SOURCE[0]}")" rev-parse --show-toplevel)" +cd "$ROOT" +CONTAINER="${OSCX_ENGINE_CONTAINER:-oscx-engine}" +ARM_ENGINE_OUT="${ARM_ENGINE_OUT:-/work/engine/engine/src/out/linux_release_arm64}" + +echo "[arm-restore] 1/5 shell + app AOT snapshots (aarch64)" +cp "apps/oscortex_app/build/oscortex/libapp-arm64.so" initramfs/system/flutter/libapp.so +cp "apps/oscortex_files/build/oscortex/libapp-arm64.so" "initramfs/Applications/Files.app/libapp.so" +cp "apps/oscortex_canvas/build/oscortex/libapp-arm64.so" "initramfs/Applications/Canvas.app/libapp.so" +cp "apps/oscortex_web_link/build/oscortex/libapp-arm64.so" "initramfs/Applications/Web Link.app/libapp.so" + +echo "[arm-restore] 2/5 drop x86 JIT kernel_blob (arm is AOT)" +rm -f initramfs/system/flutter/flutter_assets/kernel_blob.bin initramfs/system/flutter/kernel_blob.bin +for a in Files Canvas "Web Link"; do rm -f "initramfs/Applications/$a.app/flutter_assets/kernel_blob.bin"; done + +echo "[arm-restore] 3/5 arm64 engine from the build container" +docker cp "$CONTAINER:$ARM_ENGINE_OUT/libflutter_engine.so" initramfs/system/lib/libflutter_engine.so + +echo "[arm-restore] 4/5 arm64 embedder → /init" +( cd tools/flutter-embedder && cargo +nightly build --release --target aarch64-unknown-none \ + -Z build-std=core,compiler_builtins -Z build-std-features=compiler-builtins-mem >/dev/null 2>&1 ) +cp tools/flutter-embedder/target/aarch64-unknown-none/release/oscortex-host initramfs/init +cp tools/flutter-embedder/target/aarch64-unknown-none/release/oscortex-host initramfs/bin/oscortex-host + +echo "[arm-restore] 5/5 build aarch64 kernel" +touch kernel/src/fs/initramfs.rs +cargo build --target aarch64-unknown-none -p oscortex-kernel \ + --no-default-features --features arch-aarch64 \ + -Z build-std=core,compiler_builtins,alloc -Z build-std-features=compiler-builtins-mem 2>&1 | tail -2 + +echo "[arm-restore] done. engine=$(file initramfs/system/lib/libflutter_engine.so | grep -oE 'aarch64|x86-64') libapp=$(file initramfs/system/flutter/libapp.so | grep -oE 'aarch64|x86-64') init=$(file initramfs/init | grep -oE 'aarch64|x86-64')" diff --git a/dev-tools/test/freeze-repro-x86.py b/dev-tools/test/freeze-repro-x86.py new file mode 100755 index 0000000..c72f32d --- /dev/null +++ b/dev-tools/test/freeze-repro-x86.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +# Headless freeze repro for OSCortex x86_64 (Limine ISO, pure TCG — SLOW). +# Boots the ISO, waits (patiently) for the shell, launches an app via QMP, sweeps, +# and samples present_callback to decide FROZEN vs ADVANCING. +import json, os, socket, subprocess, sys, time, signal + +ROOT = (os.environ.get("OSCORTEX_REPO") or __import__("subprocess").run(["git","rev-parse","--show-toplevel"],capture_output=True,text=True).stdout.strip()) +ISO = f"{ROOT}/oscortex.iso" +VBLK = f"{ROOT}/vdisk.img" +NVME = f"{ROOT}/nvme.img" +SERIAL = "/tmp/oscortex-x86-serial.log" +QMP = "/tmp/oscortex-x86-qmp.sock" +for p in (SERIAL, QMP): + try: os.remove(p) + except OSError: pass +for img, mb in ((VBLK, 8), (NVME, 16)): + if not os.path.exists(img): + subprocess.run(["dd", "if=/dev/zero", f"of={img}", "bs=1M", f"count={mb}"], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + +qemu = subprocess.Popen([ + "qemu-system-x86_64", "-cdrom", ISO, "-m", "2048M", "-smp", "2", + "-cpu", "qemu64,+x2apic", "-machine", "q35", + "-device", "virtio-net-pci,netdev=net0", "-netdev", "user,id=net0", + "-device", "virtio-blk-pci,drive=vblk", + "-drive", f"file={VBLK},format=raw,id=vblk,if=none", + "-device", "nvme,drive=nvmedrive,serial=oscortex0", + "-drive", f"file={NVME},format=raw,id=nvmedrive,if=none", + "-device", "qemu-xhci,id=xhci", + "-serial", f"file:{SERIAL}", "-no-reboot", "-display", "none", + "-qmp", f"unix:{QMP},server,nowait", +], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + +def reads(): + try: + with open(SERIAL, "rb") as f: return f.read() + except FileNotFoundError: return b"" +def present(): return reads().count(b"present_callback") +def has(t): return t.encode() in reads() + +def qmp_connect(timeout=300): + t0 = time.time() + while time.time() - t0 < timeout: + try: + s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM); s.connect(QMP); return s + except (FileNotFoundError, ConnectionRefusedError): time.sleep(0.5) + raise RuntimeError("QMP never came up") +qmp = qmp_connect(); buf = b"" +def rcv(): + global buf + while b"\n" not in buf: buf += qmp.recv(4096) + l, buf = buf.split(b"\n", 1); return json.loads(l) +rcv(); qmp.sendall(b'{"execute":"qmp_capabilities"}\n'); rcv() +def ev(e): qmp.sendall((json.dumps({"execute":"input-send-event","arguments":{"events":e}})+"\n").encode()); rcv() +def move(x,y): ev([{"type":"abs","data":{"axis":"x","value":x}},{"type":"abs","data":{"axis":"y","value":y}}]) +def click(x,y): + move(x,y); ev([{"type":"btn","data":{"down":True,"button":"left"}}]); time.sleep(0.1); ev([{"type":"btn","data":{"down":False,"button":"left"}}]) + +# 1. wait for shell (TCG is slow — be patient: up to 6 min) +print("[x86] waiting for shell first frames (slow TCG)...", flush=True) +t0 = time.time() +while time.time() - t0 < 360: + if has("FATAL") and has("disallowed Dart VM flag"): + print("[x86] *** FATAL: engine rejected dart-flags (UNPATCHED x64 engine) ***", flush=True) + qemu.send_signal(signal.SIGTERM); sys.exit(2) + if present() > 20: break + time.sleep(3) +pc0 = present() +print(f"[x86] shell present_callback={pc0} after {int(time.time()-t0)}s", flush=True) +if pc0 <= 20: + print("[x86] shell never rendered — abort. serial tail:", flush=True) + print(subprocess.run(["tail","-30",SERIAL],capture_output=True,text=True).stdout) + qemu.send_signal(signal.SIGTERM); sys.exit(3) + +# 2. launch an app (try a couple of grid positions; x86 res may differ) +print("[x86] launching app (grid click)...", flush=True) +for (lx,ly) in [(12380,11880),(8000,9000),(16000,9000)]: + click(lx,ly); time.sleep(8) + if present() - pc0 > 15: break +time.sleep(15) +pc_launch = present() +print(f"[x86] post-launch present_callback={pc_launch} (delta {pc_launch-pc0})", flush=True) + +# 3. sweep + sample +print("[x86] sweeping + sampling...", flush=True) +samples = [] +sweep = [(8000,8000),(14000,8000),(20000,12000),(14000,16000),(9000,14000)] +for w in range(6): + for (x,y) in sweep: move(x,y); time.sleep(0.3) + time.sleep(4) + samples.append(present()); print(f"[x86] window {w}: present={samples[-1]}", flush=True) + +advanced = samples[-1] - pc_launch +stuck = sum(1 for i in range(1,len(samples)) if samples[i]==samples[i-1]) +data = reads().decode("latin1") +crash = {m: data.count(m) for m in ["panic","KERNEL PANIC","FATAL","Unhandled Exception"] if data.count(m)>0} +print("[x86] ===== VERDICT =====", flush=True) +print(f"[x86] launch={pc_launch} final={samples[-1]} advanced={advanced} stuck={stuck}/{len(samples)-1}", flush=True) +print(f"[x86] crash markers: {crash if crash else 'NONE'}", flush=True) +ok = advanced > 5 and stuck <= 1 and not any(k in crash for k in ['panic','KERNEL PANIC','FATAL']) +print("[x86] RESULT:", "ADVANCING — no freeze (FIX WORKS)" if ok else "FROZEN / PROBLEM", flush=True) +qmp.close(); qemu.send_signal(signal.SIGTERM) +try: qemu.wait(timeout=15) +except subprocess.TimeoutExpired: qemu.kill() +sys.exit(0 if ok else 1) diff --git a/dev-tools/test/freeze-repro.py b/dev-tools/test/freeze-repro.py new file mode 100755 index 0000000..d669b31 --- /dev/null +++ b/dev-tools/test/freeze-repro.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +"""aarch64 post-app-launch freeze regression test. + +Boots the OSCortex aarch64 .kernel headless under HVF, waits for the shell to +render, launches an app via QMP pointer events, sweeps the grid, and asserts that +`present_callback` keeps advancing (the freeze symptom is present_callback +sticking ~38 frames after launch). + +Env overrides: + OSCORTEX_KERNEL path to the aarch64 .kernel (default ~/OSCortex-run/oscortex-aarch64.kernel) +Exit 0 = advancing (no freeze); non-zero = frozen / failed to render. +""" +import json, os, socket, subprocess, sys, time, signal + +KERNEL = os.environ.get("OSCORTEX_KERNEL", os.path.expanduser("~/OSCortex-run/oscortex-aarch64.kernel")) +SERIAL = "/tmp/oscortex-freeze-serial.log" +QMP = "/tmp/oscortex-freeze-qmp.sock" +for p in (SERIAL, QMP): + try: os.remove(p) + except OSError: pass + +if not os.path.exists(KERNEL): + print(f"[repro] kernel not found: {KERNEL} (set OSCORTEX_KERNEL)"); sys.exit(3) + +qemu = subprocess.Popen([ + "qemu-system-aarch64", "-M", "virt,accel=hvf", "-cpu", "host", "-smp", "1", "-m", "4G", + "-kernel", KERNEL, "-device", "ramfb", + "-device", "virtio-tablet-device", "-device", "virtio-keyboard-device", + "-display", "none", "-serial", f"file:{SERIAL}", "-qmp", f"unix:{QMP},server,nowait", +], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + +def present(): + try: + with open(SERIAL, "rb") as f: return f.read().count(b"present_callback") + except FileNotFoundError: return 0 + +def qmp_connect(timeout=40): + t0 = time.time() + while time.time() - t0 < timeout: + try: + s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM); s.connect(QMP); return s + except (FileNotFoundError, ConnectionRefusedError): time.sleep(0.3) + raise RuntimeError("QMP never came up") + +qmp = qmp_connect(); buf = b"" +def rcv(): + global buf + while b"\n" not in buf: buf += qmp.recv(4096) + line, buf = buf.split(b"\n", 1); return json.loads(line) +rcv(); qmp.sendall(b'{"execute":"qmp_capabilities"}\n'); rcv() +def ev(e): qmp.sendall((json.dumps({"execute":"input-send-event","arguments":{"events":e}})+"\n").encode()); rcv() +def move(x,y): ev([{"type":"abs","data":{"axis":"x","value":x}},{"type":"abs","data":{"axis":"y","value":y}}]) +def click(x,y): + move(x,y); ev([{"type":"btn","data":{"down":True,"button":"left"}}]); time.sleep(0.05); ev([{"type":"btn","data":{"down":False,"button":"left"}}]) + +print("[repro] waiting for shell...", flush=True) +t0 = time.time() +while time.time() - t0 < 90 and present() <= 20: time.sleep(1) +pc0 = present() +print(f"[repro] shell present={pc0} after {int(time.time()-t0)}s", flush=True) +if pc0 <= 20: + qemu.send_signal(signal.SIGTERM); print("[repro] shell never rendered"); sys.exit(3) + +click(12380, 11880); time.sleep(12) +launch = present() +print(f"[repro] post-launch present={launch}", flush=True) +samples = [] +sweep = [(8000,8000),(14000,8000),(20000,8000),(20000,14000),(14000,14000),(8000,14000)] +for w in range(6): + for (x,y) in sweep: move(x,y); time.sleep(0.15) + time.sleep(2); samples.append(present()); print(f"[repro] win{w} present={samples[-1]}", flush=True) + +advanced = samples[-1] - launch +stuck = sum(1 for i in range(1,len(samples)) if samples[i]==samples[i-1]) +ok = advanced > 5 and stuck <= 1 +print(f"[repro] launch={launch} final={samples[-1]} advanced={advanced} stuck={stuck}/{len(samples)-1}") +print("[repro] RESULT:", "ADVANCING — no freeze" if ok else "FROZEN") +qmp.close(); qemu.send_signal(signal.SIGTERM) +try: qemu.wait(timeout=10) +except subprocess.TimeoutExpired: qemu.kill() +sys.exit(0 if ok else 1) diff --git a/dev-tools/test/input-test.py b/dev-tools/test/input-test.py new file mode 100755 index 0000000..52be668 --- /dev/null +++ b/dev-tools/test/input-test.py @@ -0,0 +1,45 @@ +import json,os,socket,subprocess,time,signal +ISO=""+os.environ.get("OSCORTEX_ISO", (os.environ.get("OSCORTEX_REPO") or ".")+"/oscortex.iso")+"" +SER="/tmp/osc-x86-input.log"; QMP="/tmp/osc-x86-input-qmp.sock" +for p in (SER,QMP): + try: os.remove(p) + except OSError: pass +q=subprocess.Popen(["qemu-system-x86_64", + "-drive","if=pflash,format=raw,readonly=on,file=/tmp/ovmf-code.fd", + "-drive","if=pflash,format=raw,file=/tmp/ovmf-vars.fd", + "-cdrom",ISO,"-m","4096M","-smp","2","-cpu","qemu64","-machine","q35", + "-device","virtio-vga","-device","qemu-xhci,id=xhci", + "-device","usb-tablet,bus=xhci.0","-device","usb-kbd,bus=xhci.0", + "-serial",f"file:{SER}","-display","none","-no-reboot", + "-qmp",f"unix:{QMP},server,nowait"],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL) +def reads(): + try: return open(SER,'rb').read() + except: return b"" +def present(): return reads().count(b"present_callback") +t0=time.time() +while time.time()-t0<200 and present()<=30: time.sleep(3) +print("shell present=",present(),"after",int(time.time()-t0),"s") +s=socket.socket(socket.AF_UNIX,socket.SOCK_STREAM) +for _ in range(60): + try: s.connect(QMP); break + except: time.sleep(0.5) +buf=b"" +def rcv(): + global buf + while b"\n" not in buf: buf+=s.recv(4096) + l,buf=buf.split(b"\n",1); return json.loads(l) +rcv(); s.sendall(b'{"execute":"qmp_capabilities"}\n'); rcv() +def ev(e): s.sendall((json.dumps({"execute":"input-send-event","arguments":{"events":e}})+"\n").encode()); rcv() +def move(x,y): ev([{"type":"abs","data":{"axis":"x","value":x}},{"type":"abs","data":{"axis":"y","value":y}}]) +base=present() +# sweep abs pointer — if the kernel processes tablet input, cursor redraws → present advances +for i in range(8): + move(4000+i*3000, 6000+i*2000); time.sleep(0.6) +time.sleep(3) +after=present() +print(f"present base={base} after_sweep={after} delta={after-base}") +print("tablet detected:", b"absolute pointer (tablet)" in reads()) +print("pointer working (present advanced on moves):", after-base>3) +s.close(); q.send_signal(signal.SIGTERM) +try: q.wait(timeout=10) +except: q.kill() diff --git a/docs/FEATURE_STATUS.md b/docs/FEATURE_STATUS.md new file mode 100644 index 0000000..9214928 --- /dev/null +++ b/docs/FEATURE_STATUS.md @@ -0,0 +1,32 @@ +# OSCortex Feature Status — honest ledger + +States (per [rules.md](../rules.md) Rule 3): **DONE** (verified end-to-end on the real +artifact) · **UNVERIFIED** (built, not proven on the real artifact) · **STUB** +(plumbing/UI exists, core behavior faked/missing) · **NOT STARTED**. + +> Honesty note: rows marked "(prior session)" were reported DONE in earlier work but +> have **not been re-verified in the current session**. Per Rule 6 they should be +> treated as UNVERIFIED until someone re-runs them. This ledger is the start of the +> stub audit the maintainer asked for — it is itself **incomplete** (see bottom). + +| Feature | State | Notes / gap | +|---|---|---| +| x86_64 boot → shell render | DONE (prior session) | UEFI ISO boots, renders Flutter shell | +| aarch64 boot → shell render | DONE (prior session) | HVF/edk2 | +| Pointer + keyboard input | DONE (prior session) | usb-tablet (x86) / virtio (arm); verified via HUD | +| **x86 app launch + render (SMP)** | **DONE (this session, headless)** | Verified `-smp 2`: Files renders, 0 panics. **UNVERIFIED on bare metal.** ~70–85% of runs render; intermittent **non-fatal** first-frame stall remains | +| x86 app launch (single core) | KNOWN-BROKEN, recoverable | Original dart:io crash; OS now survives it (resilience) but app doesn't render. SMP is the fix | +| **aarch64 app launch** | **KNOWN-BROKEN (freezes); SMP advanced 2026-06-17** | Single-core (shipped run-arm.sh): app launch freezes (same cooperative-scheduler root cause as x86). SMP work this session under HVF (real cores): the AP now runs the app (was idle); fixed a real bug — the AP never called enable_fpu_simd so CNTKCTL_EL1 was unset → the app's CNTVCT_EL0 read trapped (EC=0x18). PRECISE remaining wall: the engine bootstrap needs a pump to finish on the AP; the cooperative hand-off alone stalls, and the only pump (ISR-initiated enter_user_by_pid_noreturn_try) corrupts EL0 thread state → EC=0x24 data abort. Genuine fix = corruption-free per-CPU context switch (Redox-style switch_to) — multi-session. x86 SMP app-launch works (~70–85%); aarch64 does not yet | +| Crash auto-recovery | DONE (prior session) | kill group → refocus → relaunch | +| Package pipeline (stream/sign/install) | DONE (prior session) | Ed25519, SHA256, cache | +| Capability enforcement | DONE (prior session) | conservative (NET gated) | +| **App networking (TCP/HTTP)** | **UNVERIFIED** | 3-layer API built + compiles; **NOT e2e-verified**; DNS pending | +| **Web Link / browser** | **STUB** | App shell + webview pipeline (Dart API ↔ embedder ↔ compositor surface) work; engine is a **stub that fills a placeholder surface**. **Servo NOT integrated** — does not load real web pages. Long pole. See [browser-architecture.md](browser-architecture.md) | +| Canvas app | UNVERIFIED | launches; functional depth not audited this session | +| Files app | UNVERIFIED | launches + lists `/Applications`; file operations not audited | +| Boot/splash screen | DONE (prior session) | dot-matrix wordmark | + +## This ledger is incomplete (Rule 4 — naming the gap) +A full per-feature audit of the embedder platform channels, the app suite, and the +networking stack has **not** been done. Until it is, assume any "(prior session)" or +"UNVERIFIED" row could hide a stub. Completing this audit is a tracked task. diff --git a/docs/browser-architecture.md b/docs/browser-architecture.md new file mode 100644 index 0000000..8795be8 --- /dev/null +++ b/docs/browser-architecture.md @@ -0,0 +1,151 @@ +# OSCortex Web Browser — Architecture & Plan + +Status: **Phase 0 (foundation + scaffold).** The browser is the project's single +largest piece — realistically multiple person-months even reusing maximally. This +doc is the grounded plan from two research passes; it is the authoritative spec +for the effort. + +## Decision: Servo, not Chromium + +The directive was "embed prebuilt Chromium so we don't rebuild." That premise does +not survive contact with a microkernel, and the alternatives were evaluated: + +- **Prebuilt Chromium/CEF — impossible.** The Linux binaries are glibc-linked, + `dlopen` ~20+ desktop libraries (NSS, dbus, fontconfig, Mesa/GBM, …), require an + Ozone X11/Wayland display backend, and assume Chromium's multi-process + zygote + + seccomp-bpf sandbox model. OSCortex has none of that; the binary fails at + relocation before any code runs. "Don't rebuild" is the part that cannot work. +- **Chromium from source — not solo-feasible.** Requires a custom Ozone backend + + satisfying/stubbing the whole process/sandbox/GPU-process model. Google brought + Chromium to Fuchsia (their own from-scratch OS) with a first-party team over + ~3 years, and even then it is an embedded WebView, not a desktop browser. + Honest solo estimate: 3–5+ person-years, not realistically completable alone. +- **Cobalt/Starboard — wrong ceiling.** Most designed-for-porting (Blink-lineage, + BSD, POSIX-friendly), but it is a YouTube/TV-class HTML5 *app* runtime, not a + browser — it cannot browse arbitrary sites. +- **WPEWebKit — full but heavy.** A full WebKit engine that renders real sites + today, but porting onto a glibc-less microkernel drags in a heavy C/C++ tree + (ICU, EGL, GStreamer) + a custom libwpe backend. ~1.5–3 person-years. +- **Servo (chosen).** Rust (matches the kernel + toolchain + FFI), embeds as a + *single-process library* (`libservo`) — sidestepping Chromium's entire + zygote/sandbox/multi-process burden — and now has a **CPU/software rendering + path** (Vello CPU backend) suited to OSCortex's software-composited framebuffer. + Renders real sites, though compat is still maturing (Servo is officially a v0.1 + "tech demo"). Smallest porting surface, best OS-fit. ~9–18 person-months to first + real-site render solo; partial-but-improving compat thereafter. + +## Architecture: web engine as a userspace service rendering into a compositor surface + +The engine does **not** live in the kernel ("lower levels") — that would break the +microkernel model. It is a **userspace web-engine service** exposed to apps as a +native webview capability. + +**Critical finding: Flutter's external-texture API is GPU-only.** `embedder.h`'s +`FlutterEngineRegisterExternalTexture` + `TextureFrameCallback` hand back a +`FlutterOpenGLTexture`; there is no software/pixel-buffer variant, and OSCortex +runs the software renderer — so external-texture registration is rejected by the +engine. We do **not** use it. + +**Instead, the engine renders into its own OSCortex compositor surface.** OSCortex +already is a multi-surface compositor (`kernel/src/compositor/mod.rs`: per-surface +geometry, z-order, clip rect, visibility, owner-PID, `surface_at_point` hit-test). +That is the OSCortex equivalent of Android's `SurfaceProducer`/texture registry. + +``` +Web Link (Flutter app) web-engine service (libservo) + ├─ chrome surface (z=N): address bar, └─ renders page → RGBA → + │ search, back/fwd, tabs SYS_GPU_SUBMIT_STRIDED(webSurfaceId) + └─ OscWebView widget = transparent hit-rect + over the web region; forwards input + web surface (z=N-1), clipped to + oscortex/webview MethodChannel <───────> the viewport rect, composited + (control + events) directly beneath the chrome by + the OSCortex compositor +``` + +- Pixel path: service calls `create_surface_for(owner=WebLink, w, h)` → renders → + `gpu_submit_strided_for(surfaceId, rgba, row_bytes)`. Web Link places its chrome + surface at z=N and the web surface at z=N−1, clipped to the viewport + (`surface_clip_set_for`). No Flutter texture; no engine patch. +- Input: `OscWebView` wraps a `Listener` + `Focus`; hit-test in Flutter, + `globalToLocal` transform to web-local coords, forward via + `dispatchInput`/`dispatchScroll`/`dispatchKey`. (Kernel-routed input via + `surface_at_point`+owner-PID is a later optimization.) + +This mirrors webview_flutter's *texture mode* (off-screen surface + id + Texture +widget + manual gesture forwarding), substituting the OSCortex compositor surface +for the Flutter texture registry. It is **engine-agnostic**: the same interface +serves a stub today and Servo later. + +## The `oscortex/webview` contract (MethodChannel, StandardMethodCodec) + +Per-instance multiplexing via a `viewId` int in every call/event. + +**Methods (app → engine):** `create{viewId,w,h,dpr,initialUrl?}→{surfaceId}`, +`destroy`, `loadUrl{url,headers?,method?,body?}`, `loadHtml{html,baseUrl?}`, +`reload`, `stopLoading`, `goBack`, `goForward`, `canGoBack→bool`, +`canGoForward→bool`, `currentUrl→String?`, `getTitle→String?`, +`evalJs{source}→Object?`, `resize{w,h,dpr}`, `setViewport{x,y,w,h,scrollDx,scrollDy}`, +`dispatchInput{type,x,y,button,modifiers}`, `dispatchScroll{x,y,dx,dy}`, +`dispatchKey{type,keyCode,modifiers,text?}`, `setVisible{visible}`. + +**Events (engine → app):** `urlChanged`, `titleChanged`, `loadProgress{0..100}`, +`loadStarted`, `loadFinished`, `loadError{code,description,url}`, +`navState{canGoBack,canGoForward}`, `scrollChanged{x,y}`, `frameReady{surfaceId}`. + +Input type/button/modifier enums reuse the existing `EV_POINTER`/`EV_KEY`/`EV_SCROLL` +vocabulary in `kernel/src/embedder/abi.rs`. Transport reuses the platform-channel +mailbox (`kernel/src/platform_channel/mod.rs`). + +## Phased plan + +Two tracks. Track B (scaffold) is engine-agnostic and de-risks the integration +before the expensive Track A (engine) investment; they proceed in parallel. + +**Track B — integration scaffold (tractable, weeks):** +1. `packages/oscortex_webview/` — `OscWebViewController` (the channel API above) + + `OscWebView` widget (placeholder + input forwarding) + unit tests. ← *first code* +2. Wire **Web Link** (`apps/oscortex_web_link`, today a pure mockup) into a real + browser chrome: address bar → `loadUrl`, back/fwd buttons gated by `navState`, + progress/title from events, the `OscWebView` over the web region. +3. Embedder handler for `oscortex/webview` (StandardMethodCodec; mirrors the + mousecursor/clipboard handlers) routing to the web-engine service. +4. **Stub web-engine service** — a userspace process that creates the surface and + renders a placeholder (URL text / solid fill) so the *entire pipeline* + (app → channel → surface → composite → input → events) is provable end-to-end + without Servo. This validates the design. + +**Track A — Servo bring-up (the long pole, months):** +1. Cross-compile `libservo` for the OSCortex target with the **Vello CPU / software + render path** + WebRender on a software-GL or CPU surface. +2. Implement the minimal embedder glue: window/surface = the OSCortex compositor + surface; input = the forwarded events; reuse the timer/thread/epoll/fs/net shims + the Flutter embedder already provides. +3. **Make-or-break milestone:** render a single static HTML page to the surface, + headless, and diff against a reference PNG (the same way the Flutter shell is + validated). Everything after is incremental web-compat + interactivity. +4. Replace the stub service with libservo behind the same `oscortex/webview` + contract. + +**Final step (already ready):** compile Web Link with the `osx` toolchain +(`osx build` → universal `.osx`) and install it as the system browser app. This is +the easy end — it works today; it is gated entirely on the engine underneath. + +## Honest effort + +Track B: weeks. Track A (Servo to first real-site render): ~9–18 person-months +solo; genuinely usable compat is multi-year and tracks upstream Servo's own +maturation. This is the largest single undertaking in the project. + +## Sources + +Feasibility: Chromium sysroot/glibc baseline & Linux deps, Ozone/GBM, Linux +sandbox/zygote ([chromium.googlesource.com](https://chromium.googlesource.com/chromium/src/+/main/docs/linux/build_instructions.md)); +Cobalt/Starboard ([github.com/youtube/cobalt](https://github.com/youtube/cobalt)); +Servo `libservo` + Vello CPU backend + servo-shot ([servo.org](https://servo.org/blog/2025/08/22/this-month-in-servo/)); +WPEWebKit ([wpewebkit.org](https://wpewebkit.org/blog/02-overview-of-wpe.html)); +Chromium-on-Fuchsia ([chromium.googlesource.com/.../fuchsia_web](https://chromium.googlesource.com/chromium/src/+/HEAD/fuchsia_web/)). +Integration: Flutter `embedder.h` external-texture (GPU-only) & compositor/platform-view +([github.com/flutter/engine](https://github.com/flutter/engine/blob/main/shell/platform/embedder/embedder.h)); +webview_flutter texture-mode vs hybrid-composition & `SurfaceProducer` +([docs.flutter.dev](https://docs.flutter.dev/release/breaking-changes/android-surface-plugins)). diff --git a/docs/releases/v0.1.4.md b/docs/releases/v0.1.4.md new file mode 100644 index 0000000..9257c06 --- /dev/null +++ b/docs/releases/v0.1.4.md @@ -0,0 +1,58 @@ +# OSCortex v0.1.4 — x86 app-launch under SMP (the long-open "2nd-engine" blocker is fixed) + +## Headline +Launching a bundled app on x86 no longer takes down the OS. Under multi-core (SMP), +a launched app (Files, Web Link, Canvas) now **initialises its own Flutter engine and +renders its full UI on a dedicated application core**, while the shell keeps running on +the boot core. This was the multi-session blocker (every app launch used to crash the +shell's dart:io event handler and freeze the machine). + +## How it works +Each launched engine's process group is pinned to its own CPU (**home-core isolation**), +so it runs on separate per-CPU register state and never shares the cooperative-scheduler +context that corrupted a second engine on a single core. + +## Requirements (important for testing) +- **This build needs ≥2 CPU cores.** On a real Intel Mac (bare metal) that's automatic. + In UTM/QEMU, set the VM to **2+ cores** (`-smp 2` or higher). +- On a single core, the multi-core isolation is inert and app-launch falls back to the + old behaviour (the app crashes) — but the OS now **survives** that crash (see below) + rather than hanging. + +## Stability +- **The OS is crash-proof under SMP**: 0 kernel panics / 0 page-table corruption across + the headless launch test runs with these fixes. A faulting app is torn down and the + shell keeps running. +- App launch+render succeeds in the large majority of headless runs. An **intermittent + first-frame stall** remains (the app or shell occasionally initialises but pumps few + frames) — it is **non-fatal**: the machine stays alive; relaunch/reboot renders. + +## Web Link (browser) — scaffold, not a working browser yet +Honest status: **the browser does not browse real web pages yet.** What ships: +- The **Web Link app launches and renders its chrome** + a placeholder web surface. +- The **end-to-end webview pipeline is proven**: the app-side Dart API + (`oscortex_webview`: loadUrl/back/forward/reload/title/JS/input/resize over the + `oscortex/webview` MethodChannel) ↔ the embedder's native webview channel handler ↔ + an OSCortex compositor surface z-stacked beneath the app chrome. A **stub engine** + currently fills that surface and emits URL/progress/nav events, which validates the + whole path without a real web engine. +- **Not integrated:** the actual web rendering engine (**Servo**) — this is the + months-long long pole and is the next major effort. Architecture in + `docs/browser-architecture.md`. + +So: the browser's OS-side plumbing + app shell are done and demonstrable; the web engine +behind it is not. + +## SMP-safety fixes under the hood (latent bugs the single-core kernel had hidden) +- Per-CPU page-table lock (a global re-entrancy counter let a 2nd core bypass the lock + → parallel page-table writes → corruption). +- Fixed a PTABLE↔window-manager ABBA lock-order deadlock. +- Reschedule IPI on spawn so an application core wakes immediately. +- Page-table walks degrade to "not mapped" instead of panicking when crash-recovery has + freed an address space another core still references. + +## Notes +- **Single-core builds (the prior default) are byte-unchanged.** +- Built from `feat/native-engine-port`. **Not yet confirmed on bare-metal hardware** — + this release is the vehicle for that test. +- Assets: `oscortex-x86_64-v0.1.4.iso` (+ `.sha256`). diff --git a/docs/smp-architecture.md b/docs/smp-architecture.md new file mode 100644 index 0000000..7aaf270 --- /dev/null +++ b/docs/smp-architecture.md @@ -0,0 +1,97 @@ +# OSCortex SMP — Multi-Core Scheduler Architecture & Roadmap + +Status: **single-core by default** (the `smp` Cargo feature gates AP bring-up off — see +`kernel/src/arch/*/smp.rs`). This document is the principled plan to make OSCortex +*actually scale across cores*, replacing the cooperative single-core scheduler with a +Redox-style per-CPU preemptive one. It exists because six prior patch-and-pray SMP +attempts failed for lack of a plan; build to this, incrementally, never breaking the +single-core path. + +## Why single-core today + +The post-app-launch freeze is a multi-threaded Dart-VM GC **stop-the-world safepoint** +that cannot converge on one core (the GC threads need to run *simultaneously* to all +reach the safepoint). Single-core preemption was tried — it does not fix it. Multi-core +is the real fix. But naively turning APs loose on the current scheduler corrupted state +(6 attempts), and AP bring-up itself hung a real Skylake at boot. So: single-core is the +*stable* config now; SMP is a methodical foundation upgrade. + +## Current model (what we're replacing) + +- **User scheduling = cooperative hand-off.** `enter_user_by_pid_noreturn(pid)` restores a + process's full context and *never returns*; every blocking primitive (futex, cond, + mutex, epoll, fd) yields by calling it. Wakers **directly enter the woken thread**. +- **Preemption = timer-ISR frame rewrite.** The timer ISR saves the full trap frame, + calls `timer_preempt_switch_try` to pick the next thread, overwrites the live frame, and + returns into it. EL0 preemption is currently **disabled on aarch64** (`preempt_enabled = + false`) and quantum-gated on x86. +- **Global lock.** `PTABLE_LOCK` (recursive, per-CPU holder + recursion counts) guards the + process table; ISR paths use `try_lock` to stay deadlock-free. +- **Sync.** Futex table is `BTreeMap>` with a `get_group_leader` address-space + filter (interim cross-process fix). Cond/mutex are cooperative + a `force_wake_all_task_ + runners` deadlock-breaker that pokes every timerfd. +- A cooperative kernel `context_switch(old_sp, new_sp)` (callee-saved only) **already + exists** on both arches — this is the `switch_to_inner` primitive Redox uses. + +## Target model (Redox blueprint, adapted) + +1. **Per-CPU scheduler state + affinity run queue** — each CPU has its own `current`/`idle` + context; thread selection filters by `sched_affinity`. (We already have sticky + `current_cpu` affinity + per-CPU `current_pid`.) +2. **`CONTEXT_SWITCH_LOCK` + non-preemptable bail-out** — one atomic serializes switches; + never switch *away* from a thread that is non-preemptable (holding a kernel lock, or at + a non-safepoint). This is the piece that corrupted preempted Dart threads. +3. **Two-layer `switch_to(prev,next)` / naked `switch_to_inner`** — exact callee-saved + + FP/cr3/ttbr save sets. (`context_switch` is the inner half; add the high-level wrapper.) +4. **Physical-address-keyed futex** — key the waiter table by the *physical* address of the + futex word (+ address-space validation), replacing VA+group-scoping. Lock-free vs + `PTABLE_LOCK`; correct across cores and across the shell/app shared-VA collision. +5. **IPI-based wakeups** — wakers `set_state(Running)` + send a reschedule IPI to the + target core; they do **not** jump into the woken thread. (x86 `broadcast_resched_ipi` + exists; aarch64 needs the GIC SGI.) + +## Milestones (each keeps single-core working; AP code gated on the `smp` feature) + +- **M0 ✅ AP bring-up → idle.** Secondary cores boot and park (done; gated off by default). +- **M1 ⏳ Safe-preemption core: `preempt_disable()` + `CONTEXT_SWITCH_LOCK`.** Per-CPU + preempt-disable depth (auto-engaged while `PTABLE_LOCK` is held); the timer-preempt path + bails when this CPU is non-preemptable, so we never switch away from a global-lock holder. + Additive + dormant where preemption is off; real effect on the x86 quantum-preempt path. + *Verify: both arches still boot + render single-core, no regression.* +- **M2 ✅ Physical-address-keyed futex.** `futex_wake_waiters` now scopes by the PHYSICAL + address of the futex word (`futex_phys_of` = translate the waiter's VA via its page-table + root), the address-space-independent identity; falls back to group-leader scoping only if + a translation fails (never drops a wake the old path allowed). Behaviorally identical + while apps share the shell's address space (same pml4 → same physaddr → same decision); + becomes the correct key once engines are isolated into separate address spaces. *Verified: + engine bring-up boots + renders the launcher (present→65, 0 faults).* NOTE: still takes + `PTABLE_LOCK` to read each waiter's pml4 (`pml4_phys_of`) — fully lock-free lookup is a + later optimization; correctness/keying is the M2 deliverable. +- **M3 ✅ Reschedule-IPI primitive on both arches.** x86 was already complete + (`broadcast_resched_ipi` → `send_resched_ipi` → vector 0x40 → `apic_resched_handler`), + and `set_state(Running)` already calls `broadcast_resched_ipi` on a wake. Filled the + aarch64 gap: `gic::send_sgi_all_but_self`/`send_sgi` (write GICD_SGIR), `SGI_RESCHED` + (SGI 0), `broadcast_resched_ipi` now sends it (guarded on `CPU_COUNT > 1` so single-core + never touches the GIC — it's on the hot set_state path), and the IRQ handler recognizes + + EOIs SGI 0. *Compile-verified both arches × {default, smp}; provably single-core-neutral + (guarded no-op + the SGI-0 branch is unreachable on one core). SGI delivery + the actual + rerun-scheduler-on-IPI action are exercised at M5 (APs idle until then).* NOTE: the wake + sites still use the cooperative direct-enter on the same core; M3 adds the CROSS-core + signal — replacing direct-enter wholesale waits for M4/M5's per-CPU run queues. +- **M4 Two-layer `switch_to` + per-CPU current/idle contexts.** Build the high-level switch + over `context_switch`; give each CPU an idle context. *Verify: BSP uses it with no + regression; an AP runs a per-CPU idle loop (heartbeat log) under `--features smp`.* +- **M5 Turn APs loose + verify the freeze is gone.** APs pull runnable threads (affinity + run queue); foreground app's engine threads distribute across cores; GC safepoint + converges. *Verify: launch an app under `-smp 2/4`, sweep, no freeze; then UTM + Mac.* + +## Hard rules +- Never break the single-core boot. All AP/SMP-only code is behind `#[cfg(feature="smp")]` + or a `CPU_COUNT > 1` runtime check. +- Verify every milestone in QEMU (both arches) before committing; only verified work ships. +- `is_preemptable`/non-preemptable bail is the safety net for preempting Dart — do not + enable EL0 preemption broadly until safepoint cooperation or the bail covers it. + +See also: kernel/src/process/mod.rs (scheduler), kernel/src/arch/*/smp.rs (per-CPU + AP), +kernel/src/syscall/handlers/futex.rs (futex), kernel/src/arch/*/{idt,apic,vectors}.rs +(preemption). Memory: the Redox patterns + prior-attempt log live in the agent's notes. diff --git a/engine-port/artifact.config b/engine-port/artifact.config index c7a283f..7ebed5d 100644 --- a/engine-port/artifact.config +++ b/engine-port/artifact.config @@ -9,7 +9,7 @@ # BUMP `ARTIFACT_VERSION` whenever engine-port/ changes or FLUTTER_REV is bumped, # so consumers pull a fresh artifact instead of a stale cached one. -ARTIFACT_VERSION="oscortex-engine-1" +ARTIFACT_VERSION="oscortex-engine-2" # Upstream Flutter these artifacts are built from (pinned/frozen — we do not # track upstream). For reproducibility. diff --git a/engine-port/patches/0004-switches-allow-gc-flags.patch b/engine-port/patches/0004-switches-allow-gc-flags.patch new file mode 100644 index 0000000..a9c8d5b --- /dev/null +++ b/engine-port/patches/0004-switches-allow-gc-flags.patch @@ -0,0 +1,22 @@ +diff --git a/engine/src/flutter/shell/common/switches.cc b/engine/src/flutter/shell/common/switches.cc +index 15d43a5f..e098116f 100644 +--- a/engine/src/flutter/shell/common/switches.cc ++++ b/engine/src/flutter/shell/common/switches.cc +@@ -42,6 +42,17 @@ struct SwitchDesc { + static const std::string kAllowedDartFlags[] = { + "--enable-isolate-groups", + "--no-enable-isolate-groups", ++ // OSCortex: GC/heap tuning REQUIRED on the cooperative single-core scheduler ++ // (default multi-threaded GC safepoint never converges for a 2nd app engine, ++ // freezing the render thread). Prefix-matched, so "=value" forms are covered. ++ "--old_gen_heap_size", ++ "--new_gen_semi_max_size", ++ "--scavenger_tasks", ++ "--marker_tasks", ++ "--concurrent_mark", ++ "--concurrent_sweep", ++ "--no_concurrent_mark", ++ "--no_concurrent_sweep", + }; + // clang-format on + diff --git a/engine-port/publish-engine.sh b/engine-port/publish-engine.sh index bd17b01..e05b6b0 100755 --- a/engine-port/publish-engine.sh +++ b/engine-port/publish-engine.sh @@ -23,8 +23,15 @@ CONTAINER="oscx-engine" ARCH="${1:-x64}" MODE="${2:-release}" NAME="oscortex-${ARCH}-${MODE}" -OUTDIR="out/oscortex_release" -[ "$MODE" = "debug" ] && OUTDIR="out/oscortex_debug_unopt" +# GN out-dir per target. The dir names differ by arch (arm64 was configured as a +# plain linux release; x64 via the oscortex target), so map explicitly rather +# than assuming a single dir. +case "$ARCH-$MODE" in + arm64-release) OUTDIR="out/linux_release_arm64" ;; + x64-release) OUTDIR="out/oscortex_release_x64" ;; + x64-debug) OUTDIR="out/oscortex_debug_unopt_x64" ;; + *) OUTDIR="out/oscortex_${MODE}" ;; +esac R2_BUCKET="${R2_BUCKET:-oscortex-engine}" KEY="$ARTIFACT_VERSION/${NAME}.tar.gz" diff --git a/iso_root/limine.conf b/iso_root/limine.conf deleted file mode 100644 index a181541..0000000 --- a/iso_root/limine.conf +++ /dev/null @@ -1,11 +0,0 @@ -# OSCortex boot configuration -timeout: 0 -serial: yes -verbose: yes - -/OSCortex AI-First Kernel - protocol: limine - path: boot():/boot/kernel - kaslr: no - module_path: boot():/boot/libflutter_engine.so - module_cmdline: libflutter_engine.so diff --git a/kernel/Cargo.toml b/kernel/Cargo.toml index 965341b..9ac6236 100644 --- a/kernel/Cargo.toml +++ b/kernel/Cargo.toml @@ -26,6 +26,8 @@ smoltcp = { version = "0.12", default-features = false, features = [ "socket-tcp", "socket-icmp", "socket-dhcpv4", + "socket-dns", + "proto-dns", "medium-ethernet", "log", "iface-max-route-count-4", @@ -33,6 +35,9 @@ smoltcp = { version = "0.12", default-features = false, features = [ "iface-max-addr-count-4", "iface-max-multicast-group-count-4", "assembler-max-segment-count-4", + "dns-max-name-size-255", + "dns-max-result-count-2", + "dns-max-server-count-2", ] } [features] @@ -47,3 +52,11 @@ limine-boot = [] ai-cortex = [] driver-wasm = [] self-healing = [] +# Multi-core (SMP) AP bring-up. OFF by default: the scheduler isn't fully SMP-safe +# yet and AP bring-up hung real hardware (a 2016 Skylake Mac) at cortex::smp. The +# proven-stable config is single-core (serial-GC engine). Enable for SMP dev. +smp = [] +# Draw an on-screen input debug HUD (bound device + live pointer/key/scroll event +# counts) over every frame. For diagnosing input on a VM/board with no serial. +# Off in release ISOs (zero cost). See compositor::input_hud + dev-tools/README.md. +input-hud = [] diff --git a/kernel/src/app_registry/mod.rs b/kernel/src/app_registry/mod.rs index bde2985..80367cd 100644 --- a/kernel/src/app_registry/mod.rs +++ b/kernel/src/app_registry/mod.rs @@ -485,6 +485,17 @@ pub fn launch(app_id: u32, _flags: u32) -> i64 { // aot_va=0: the embedder ignores it in APP mode when is_aot=false. crate::process::set_bootstrap_regs(child_pid, HOST_MODE_APP, app_id as u64, 0); + // SMP: the app host was pinned to a dedicated APPLICATION core inside + // spawn_with_bootstrap (atomically, before it became Running — see the + // home_cpu assignment there). On single-core boots that pin is 0 (the BSP), + // so this is purely informational. The home_cpu is inherited by every thread + // the host spawns (see spawn_thread), so the whole engine stays on its core, + // with its OWN per-CPU GPR scratch — eliminating the cooperative-context + // corruption that crashed the shell's dart:io EventHandler when a 2nd engine + // launched on a single shared core. + log::error!("[APP] app_id={} host_pid={} home_cpu={}", + app_id, child_pid, crate::process::home_cpu_of(child_pid)); + // Track the running instance for crash auto-recovery (see note_thread_exit). note_running(child_pid, app_id); diff --git a/kernel/src/arch/aarch64/apic.rs b/kernel/src/arch/aarch64/apic.rs index ece7eb8..8b7ff35 100644 --- a/kernel/src/arch/aarch64/apic.rs +++ b/kernel/src/arch/aarch64/apic.rs @@ -72,6 +72,14 @@ pub fn start_scheduler_tick() { fn production_irq_handler(f: &mut super::vectors::TrapFrame) { let intid = super::gic::acknowledge(); + // Cross-core reschedule IPI (SMP, SGI 0). Taking the interrupt already woke + // this core from wfi; EOI and return. Once APs run the scheduler (M5), this is + // where the woken core re-picks a runnable thread. + if intid == super::gic::SGI_RESCHED { + super::gic::eoi(intid); + return; + } + // Non-timer interrupts. virtio-mmio input SPIs (slot i → INTID 48+i) drive the // pointer/keyboard; drain them into the WM queue, then EOI. Everything else is // spurious/unhandled: EOI (if real) and return. @@ -90,6 +98,16 @@ fn production_irq_handler(f: &mut super::vectors::TrapFrame) { // EOI now so the GIC can deliver the next timer interrupt right after eret. super::gic::eoi(intid); + // SMP: a secondary core that has NOT yet engaged user scheduling does ZERO + // work here (just the EOI above, which also wakes it from wfi to re-poll). This + // keeps an idling AP from touching PTABLE_LOCK / the wake machinery and thus + // from contending with the BSP during the engine's single-core bring-up — the + // contention that previously broke app launch under smp>1. The BSP (cpu 0) is + // always engaged. + if !super::smp::cpu_engaged(super::smp::current_cpu_id()) { + return; + } + // Wake threads whose timerfd deadlines have elapsed, and deliver any pending // cross-thread wakes — exactly as the x86 APIC-timer ISR does (idt.rs). On // aarch64 this was missing, so the engine's UI/raster/IO worker threads that @@ -125,7 +143,11 @@ fn production_irq_handler(f: &mut super::vectors::TrapFrame) { // engine schedules frames forever (schedule_frame label=idle) but the baton is // never delivered, so FlutterEngineOnVsync is never called and no frame is // ever produced or presented. (aarch64 had only a no-op vsync_due() scaffold.) - if vsync_due() { + // GLOBAL per-tick work (vsync baton, WM tick, USB poll) runs on the BSP ONLY — + // these are single-driver subsystems; letting a 2nd core drive them too would + // double-deliver vsync batons / double-poll USB. APs only do per-CPU scheduling + // below. + if super::smp::current_cpu_id() == 0 && vsync_due() { crate::wm::tick(); // Deliver the vsync baton ONLY — do NOT call compositor::tick() here: its // render_frame() does a full-screen blit/fill that, in the timer ISR under @@ -160,16 +182,39 @@ fn production_irq_handler(f: &mut super::vectors::TrapFrame) { let cur = crate::process::current_pid(); let focus = crate::wm::focus_pid(); let target = if focus != 0 { focus } else { 1 }; - if target != 0 + // HOME-CORE per-core wake-assist (mirrors x86 idt.rs). Only the target's HOME + // core re-enters it: the BSP pumps the shell (home 0), an AP pumps the app + // pinned to it (home 1+). try_claim_cpu_for_try is home-gated so this can't + // steal another core's thread. The ISR-initiated resume below was previously + // disabled (// BISECT) as the suspected cause of "pid=2 Dart corruption" — but + // that predates the 2026-06-16 per-CPU page-table-lock + ABBA lock-order fixes, + // the real corruption source. Re-enabled: the app's engine bootstrap stalls at + // FlutterEngineRunInitialized without it (the worker the isolate launch waits + // on is parked and only this resume re-enters it), exactly as on x86. + let my_cpu = super::smp::current_cpu_id(); + let target_is_mine = crate::process::home_cpu_of_try(target) == Some(my_cpu); + if target != 0 && target_is_mine && (cur == 0 || crate::process::is_blocked_try(cur)) && (crate::wm::input_pending_for(target) > 0 || crate::wm::embedder_baton_due(target)) && cur != target { - // BISECT: just wake the target; let normal cooperative scheduling enter - // it (the ISR-initiated resume enter_user_by_pid_noreturn_try is the - // prime suspect for the residual pid=2 Dart corruption — disabled to test). - let _ = crate::process::set_state_try(target, crate::process::ProcState::Running); + // The cooperative hand-off alone does NOT re-enter the force_wake-released + // engine thread on the AP, so the bootstrap stalls without an ISR resume. + // The resume corrupts ONLY when it re-enters a thread with no valid saved + // FP (build_image zeroes v0–v31 incl. callee-saved v8–v15 → EC=0x24). So: + // resume MULTI-CORE only, and ONLY when the target has a valid FP image + // (aarch64_fp_valid) — i.e. exactly the case where restoring FP is correct. + // No valid FP → wake-only (skip), never enter with garbage FP. + if crate::process::set_state_try(target, crate::process::ProcState::Running) { + let smp = crate::arch::smp::CPU_COUNT.load(Ordering::Acquire) > 1; + if smp + && crate::process::aarch64_fp_valid(target) + && crate::process::try_claim_cpu_for_try(target, my_cpu) + { + crate::process::enter_user_by_pid_noreturn_try(target); + } + } } return; } @@ -184,12 +229,16 @@ fn production_irq_handler(f: &mut super::vectors::TrapFrame) { let slice_expired = crate::process::account_tick_try(cur).unwrap_or(false); let focus = crate::wm::focus_pid(); let target = if focus != 0 { focus } else { 1 }; - // Cooperative-only scheduling (matches x86, which never preempts user threads - // and renders reliably). The timer ISR still drives vsync/timerfd-wake/kick - // above; we only suppress the preemptive *thread switch*. Preempting an engine - // thread mid-execution was the cause of the residual Dart EventHandler crash; - // now that the syscall-arg3 re-exec corruption is fixed, the engine is driven - // entirely by cooperative syscall yields and no longer needs preemption. + // EL0 preemption stays OFF — empirically it does NOT fix the post-app-launch + // freeze and currently can't even fire: in a 45s render test the timer ISR + // landed in EL1 (kernel) on EVERY tick (arm-preempt-tick=0), because the threads + // spend ~all their time in EL1 syscalls / cooperative-handoff loops. And the + // freeze itself is a multi-threaded Dart GC stop-the-world safepoint that can't + // converge on ONE core — the deadlocked engine threads are blocked in EL1 + // cond_wait/epoll, which EL0 preemption can't touch. The real fixes are SMP + // (parallel cores → the GC converges) or serial GC (engine allowlist patch); see + // [[post-app-launch-freeze]]. Preemption is still wanted for the general + // starvation class, but needs EL0 IRQ delivery wired first (separate work). let preempt_enabled = false; let should_preempt = preempt_enabled && (slice_expired diff --git a/kernel/src/arch/aarch64/fdt.rs b/kernel/src/arch/aarch64/fdt.rs index b857a4d..4b00970 100644 --- a/kernel/src/arch/aarch64/fdt.rs +++ b/kernel/src/arch/aarch64/fdt.rs @@ -255,6 +255,76 @@ pub unsafe fn parse(dtb_pa: u64) -> Option { } } +/// PSCI call conduit advertised by the DTB `/psci` node's `method` property. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum PsciConduit { + Hvc, + Smc, +} + +/// Read the `/psci` node's `method` ("hvc"/"smc") from the DTB. This is the +/// authoritative way to know how to issue PSCI calls (CPU_ON for SMP) — guessing +/// the conduit and getting it wrong traps/hangs. Returns `None` if absent. +/// +/// # Safety +/// `dtb_pa` must point at a DTB blob accessible at this address. +pub unsafe fn psci_method(dtb_pa: u64) -> Option { + let hdr = FdtHeader::parse(dtb_pa as *const u8)?; + let mut p = hdr.struct_start(); + let end = hdr.struct_end(); + let mut depth: i32 = -1; + let mut in_psci = false; + while p < end { + let token = read_be32(p); + p = p.add(4); + match token { + t if t == FDT_BEGIN_NODE => { + let name_ptr = p; + let mut nlen = 0usize; + while *name_ptr.add(nlen) != 0 { + nlen += 1; + } + let name = core::str::from_utf8(core::slice::from_raw_parts(name_ptr, nlen)) + .unwrap_or(""); + p = p.add((nlen + 4) & !3); + depth += 1; + if depth == 1 { + in_psci = name == "psci" || name.starts_with("psci@"); + } + } + t if t == FDT_END_NODE => { + if depth == 1 { + in_psci = false; + } + depth -= 1; + } + t if t == FDT_PROP => { + let len = read_be32(p); + let nameoff = read_be32(p.add(4)); + let val = p.add(8); + let pname = hdr.string_at(nameoff); + if in_psci && pname == "method" && len > 0 { + let s = core::str::from_utf8(core::slice::from_raw_parts( + val, + (len as usize).saturating_sub(1), + )) + .unwrap_or(""); + return Some(if s.starts_with("smc") { + PsciConduit::Smc + } else { + PsciConduit::Hvc + }); + } + p = p.add(((len as usize) + 3) & !3); + } + t if t == FDT_NOP => {} + t if t == FDT_END => break, + _ => break, + } + } + None +} + /// Scan a physical address range for the FDT magic on an 8-byte stride and /// return the first address whose blob parses to a valid `/memory` node. /// diff --git a/kernel/src/arch/aarch64/gic.rs b/kernel/src/arch/aarch64/gic.rs index 4c7914f..871cd59 100644 --- a/kernel/src/arch/aarch64/gic.rs +++ b/kernel/src/arch/aarch64/gic.rs @@ -18,6 +18,11 @@ const GICD_ICENABLER: u64 = 0x180; // Clear-enable const GICD_IPRIORITYR: u64 = 0x400; // Priority (1 byte/IRQ) const GICD_ITARGETSR: u64 = 0x800; // CPU targets (1 byte/IRQ, SPIs only) const GICD_ICFGR: u64 = 0xC00; // Config (2 bits/IRQ: edge/level) +const GICD_SGIR: u64 = 0xF00; // Software-generated interrupt (send an SGI) + +/// SGI id used for the cross-core reschedule IPI (SMP). SGIs are 0..15; nothing +/// else here uses one. +pub const SGI_RESCHED: u32 = 0; // CPU interface registers (offsets from GICC_BASE). const GICC_CTLR: u64 = 0x00; // CPU interface control @@ -102,6 +107,22 @@ pub fn route_to_cpu0(intid: u32) { } } +/// Send a Software-Generated Interrupt to **all other** CPU interfaces (the +/// reschedule IPI). Uses TargetListFilter=0b01 ("all but self"), so on a +/// single-core system it reaches no one — a safe no-op. `sgi_id` is 0..15. +pub fn send_sgi_all_but_self(sgi_id: u32) { + // GICD_SGIR: [25:24]=TargetListFilter (0b01 = all-but-self), + // [23:16]=CPUTargetList (ignored for filter 0b01), [3:0]=SGIINTID. + let val = (0b01u32 << 24) | (sgi_id & 0xF); + unsafe { gicd_write(GICD_SGIR, val) }; +} + +/// Send an SGI to a specific set of CPU interfaces (bitmask, bit N = interface N). +pub fn send_sgi(target_mask: u8, sgi_id: u32) { + let val = ((target_mask as u32) << 16) | (sgi_id & 0xF); + unsafe { gicd_write(GICD_SGIR, val) }; +} + /// Acknowledge the pending interrupt; returns its INTID (1023 = spurious). pub fn acknowledge() -> u32 { unsafe { gicc_read(GICC_IAR) & 0x3FF } diff --git a/kernel/src/arch/aarch64/smp.rs b/kernel/src/arch/aarch64/smp.rs index 70ab1be..c5f8b5b 100644 --- a/kernel/src/arch/aarch64/smp.rs +++ b/kernel/src/arch/aarch64/smp.rs @@ -21,6 +21,11 @@ pub struct PerCpuData { pub mpidr: u64, pub online: AtomicBool, pub current_pid: AtomicU32, + /// An AP is "engaged" once it has joined user scheduling. While NOT engaged + /// (online but idling pre-engage), its timer ISR does ZERO scheduler/lock work + /// (just EOI) so it can't contend PTABLE_LOCK with the BSP during the engine's + /// single-core bring-up. The BSP (cpu 0) is always considered engaged. + pub engaged: AtomicBool, } const ZERO_CPU: PerCpuData = PerCpuData { @@ -28,12 +33,27 @@ const ZERO_CPU: PerCpuData = PerCpuData { mpidr: 0, online: AtomicBool::new(false), current_pid: AtomicU32::new(0), + engaged: AtomicBool::new(false), }; static mut PER_CPU_DATA: [PerCpuData; MAX_CPUS] = [ZERO_CPU; MAX_CPUS]; /// Total logical CPUs online (including BSP). pub static CPU_COUNT: AtomicU32 = AtomicU32::new(1); +/// Set true by the BSP once it has claimed the init process and is entering +/// userspace — APs must not run the scheduler before this (else they'd race the +/// BSP to claim the init thread). Set inside `enter_user_by_pid_noreturn` on CPU 0. +pub static SMP_SCHED_READY: AtomicBool = AtomicBool::new(false); + +#[inline] +pub fn sched_ready() -> bool { + SMP_SCHED_READY.load(Ordering::Acquire) +} +#[inline] +pub fn set_sched_ready() { + SMP_SCHED_READY.store(true, Ordering::Release); +} + /// Read this core's MPIDR_EL1 affinity bits. #[inline] fn read_mpidr() -> u64 { @@ -70,10 +90,345 @@ pub fn this_cpu() -> &'static PerCpuData { unsafe { &(*core::ptr::addr_of!(PER_CPU_DATA))[cpu_idx] } } -/// Wake all APs. Currently single-core only. +// ── SMP Milestone 1: AP bring-up via PSCI CPU_ON ───────────────────────────── +// +// Secondary cores park in boot.rs `_start` (wfe). We wake them with PSCI CPU_ON +// (conduit read from the DTB) pointing at `_ap_trampoline` (a PHYSICAL address — +// the `-kernel` image is identity-mapped, VA==PA). The trampoline runs MMU-off, +// replicates the BSP's MMU/system state captured in `capture_boot_state`, turns +// the MMU on, then calls `ap_main`. Milestone 1: the AP just inits its GIC CPU +// interface, marks itself online, and idles — it touches NO shared user/scheduler +// state yet (that's Milestone 2+). The BSP keeps doing all the work; an AP coming +// online must not perturb the single-core path. + +const AP_STACK_SIZE: usize = 64 * 1024; +const MAX_APS: usize = 3; // cores 1..=3 (core 0 = BSP) + +#[repr(C, align(16))] +struct ApStacks([[u8; AP_STACK_SIZE]; MAX_APS]); +static mut AP_STACKS: ApStacks = ApStacks([[0; AP_STACK_SIZE]; MAX_APS]); + +// BSP MMU/system state the AP must load before it can run kernel C/Rust. +static mut AP_MAIR: u64 = 0; +static mut AP_TCR: u64 = 0; +static mut AP_TTBR0: u64 = 0; +static mut AP_TTBR1: u64 = 0; +static mut AP_SCTLR: u64 = 0; +static mut AP_VBAR: u64 = 0; + +/// Snapshot the BSP's live MMU + vector registers so an AP can install identical +/// translation before touching kernel memory. Call AFTER mmu::enable + vectors. +#[cfg(feature = "smp")] +pub fn capture_boot_state() { + unsafe { + let (mair, tcr, ttbr0, ttbr1, sctlr, vbar): (u64, u64, u64, u64, u64, u64); + core::arch::asm!( + "mrs {0}, mair_el1", + "mrs {1}, tcr_el1", + "mrs {2}, ttbr0_el1", + "mrs {3}, ttbr1_el1", + "mrs {4}, sctlr_el1", + "mrs {5}, vbar_el1", + out(reg) mair, out(reg) tcr, out(reg) ttbr0, + out(reg) ttbr1, out(reg) sctlr, out(reg) vbar, + options(nomem, nostack), + ); + AP_MAIR = mair; + AP_TCR = tcr; + AP_TTBR0 = ttbr0; + AP_TTBR1 = ttbr1; + AP_SCTLR = sctlr; + AP_VBAR = vbar; + } +} + +// AP entry trampoline. PSCI starts the core here MMU-off (x0 = context_id = the +// AP's logical cpu index, which we passed as the PSCI context). It drops EL2→EL1 +// if needed, enables FP, installs the BSP's MMU regs, turns the MMU on, sets VBAR, +// switches to this AP's stack, then branches to `ap_main(cpu_idx)`. +core::arch::global_asm!( + r#" +.section .text.ap_boot, "ax" +.globl _ap_trampoline +_ap_trampoline: + mov x19, x0 // x19 = cpu_idx (PSCI context_id) + // Drop EL2 -> EL1h if we came up at EL2. + mrs x0, CurrentEL + lsr x0, x0, #2 + cmp x0, #2 + b.ne 1f + mov x0, #(1 << 31) + msr hcr_el2, x0 + mov x0, #0x3c5 // DAIF masked + EL1h + msr spsr_el2, x0 + adr x0, 1f + msr elr_el2, x0 + eret +1: + msr daifset, #0xf + // Enable FP/AdvSIMD (CPACR_EL1.FPEN = 0b11). + mrs x0, cpacr_el1 + orr x0, x0, #(3 << 20) + msr cpacr_el1, x0 + isb + // Install the BSP's translation regs (MMU still OFF; symbols resolve to PA). + adrp x0, {ap_mair} + ldr x1, [x0, #:lo12:{ap_mair}] + msr mair_el1, x1 + adrp x0, {ap_tcr} + ldr x1, [x0, #:lo12:{ap_tcr}] + msr tcr_el1, x1 + adrp x0, {ap_ttbr0} + ldr x1, [x0, #:lo12:{ap_ttbr0}] + msr ttbr0_el1, x1 + adrp x0, {ap_ttbr1} + ldr x1, [x0, #:lo12:{ap_ttbr1}] + msr ttbr1_el1, x1 + isb + tlbi vmalle1 + dsb nsh + isb + // Turn the MMU on (SCTLR with M/C/I as the BSP has it). + adrp x0, {ap_sctlr} + ldr x1, [x0, #:lo12:{ap_sctlr}] + msr sctlr_el1, x1 + isb + // Vector base. + adrp x0, {ap_vbar} + ldr x1, [x0, #:lo12:{ap_vbar}] + msr vbar_el1, x1 + // Stack: AP_STACKS has slots for cores 1..=MAX_APS (slot = cpu_idx-1). The top + // of this AP's slot = base + cpu_idx * AP_STACK_SIZE. + adrp x0, {ap_stacks} + add x0, x0, :lo12:{ap_stacks} + mov x2, {stack_size} + mul x2, x2, x19 // x2 = cpu_idx * AP_STACK_SIZE + add sp, x0, x2 + mov x0, x19 // ap_main(cpu_idx) + bl {ap_main} +2: wfe + b 2b +"#, + ap_mair = sym AP_MAIR, + ap_tcr = sym AP_TCR, + ap_ttbr0 = sym AP_TTBR0, + ap_ttbr1 = sym AP_TTBR1, + ap_sctlr = sym AP_SCTLR, + ap_vbar = sym AP_VBAR, + ap_stacks = sym AP_STACKS, + ap_main = sym ap_main, + stack_size = const AP_STACK_SIZE, +); + +#[cfg(feature = "smp")] +extern "C" { + fn _ap_trampoline(); +} + +/// First Rust code on a secondary core. MMU + VBAR are live; running on this AP's +/// own stack. Milestone 1: init the per-core GIC interface, register in the +/// per-CPU table, mark online, then idle. (No scheduler entry yet.) +#[no_mangle] +pub extern "C" fn ap_main(cpu_idx: u64) -> ! { + let idx = cpu_idx as usize; + unsafe { + let table = &mut *core::ptr::addr_of_mut!(PER_CPU_DATA); + if idx < MAX_CPUS { + table[idx].cpu_id = cpu_idx as u32; + table[idx].mpidr = read_mpidr(); + table[idx].online.store(true, Ordering::Release); + } + } + // Per-core GIC CPU interface (GICv2: banked per-core at the same MMIO base). + crate::arch::aarch64::gic::init_cpu_interface(); + // CRITICAL per-core EL1 setup the AP was MISSING: enable FP/SIMD (CPACR_EL1) AND + // EL0 counter access (CNTKCTL_EL1.EL0VCTEN/EL0PCTEN). These are PER-CPU registers + // at reset on a secondary core. Without CNTKCTL, the app's inline CNTVCT_EL0 read + // (liboscortex_libc monotonic clock, called constantly by the Dart VM) traps to + // EL1 with EC=0x18 → report_unhandled → the engine bootstrap deterministically + // wedged on the AP (4/4). The x86 AP does this in ap_init; aarch64 ap_main never + // did. This is the fix for the aarch64 SMP app-launch freeze. + crate::arch::aarch64::cpu::enable_fpu_simd(); + CPU_COUNT.fetch_add(1, Ordering::AcqRel); + log::error!("[SMP] AP cpu={} ONLINE (mpidr={:#x})", cpu_idx, read_mpidr()); + + // ENGAGE this AP in HOME-CORE user scheduling (mirrors x86 sched::ap_start). + // + // History: co-scheduling was attempted 6 ways on 2026-06-14 and froze, blamed on + // a "Dart GC safepoint that won't converge" — but those attempts PREDATE the + // 2026-06-16 arch-neutral SMP-safety fixes (per-CPU page-table lock depth that + // had let a 2nd core bypass the lock → real page-table corruption; the PTABLE↔WM + // ABBA lock-order fix). The "pid=2 Dart corruption" chased back then is very + // likely those exact bugs, now fixed. The x86 home-core model (pin each WHOLE + // engine to one core + foreground-exclusive, so an engine's threads never spread + // across cores and there is no cross-core GC safepoint) launches+renders apps on + // x86 this session. Apply the same here and let the live HVF test (real parallel + // cores) say whether it converges. If it regresses, this is reverted — not + // shipped as "done". + // + // Stay FULLY DORMANT (wfi, engaged=false → timer ISR does nothing per cpu_engaged) + // until an app is actually pinned to THIS core. Engaging earlier (e.g. at + // flutter_init_ready) makes this core's timer-ISR work contend with the BSP's + // timing-sensitive shell first-frame bring-up — observed to stall the shell at + // present=1 on HVF. Apps only launch well after the shell is up, so there is + // nothing for this core to do before then. spawn_with_bootstrap sets the + // CPU_HAS_HOME_WORK flag and broadcasts a reschedule IPI (GIC SGI), which wakes + // us from wfi the instant an app is assigned here. + while !sched_ready() { + core::hint::spin_loop(); + } + // enable_and_halt (not raw wfi): IRQs enabled so the timer/SGI is actually taken + // each wake (its handler no-ops here via cpu_engaged), then we re-check the flag. + while !crate::process::cpu_has_home_work(cpu_idx as u32) { + crate::arch::enable_and_halt(); + } + unsafe { + let table = &mut *core::ptr::addr_of_mut!(PER_CPU_DATA); + if idx < MAX_CPUS { + table[idx].engaged.store(true, Ordering::Release); + } + } + log::error!("[SMP] AP cpu={} ENGAGING home-pinned scheduler (app pinned here)", cpu_idx); + loop { + // home_cpu filter inside next_runnable_pid restricts the pick to engine(s) + // pinned to THIS core, on its own per-CPU state. + if let Some(pid) = crate::process::next_runnable_pid(0) { + crate::process::enter_user_by_pid_noreturn(pid); + } + crate::arch::enable_and_halt(); + } +} + +/// True if the given CPU has engaged user scheduling (BSP/cpu 0 always true). +#[inline] +pub fn cpu_engaged(cpu_id: u32) -> bool { + if cpu_id == 0 { + return true; + } + let i = cpu_id as usize; + if i >= MAX_CPUS { + return false; + } + unsafe { (*core::ptr::addr_of!(PER_CPU_DATA))[i].engaged.load(Ordering::Acquire) } +} + +/// PSCI conduit, latched once from the DTB at wake time. +#[cfg(feature = "smp")] +static PSCI_USE_SMC: AtomicBool = AtomicBool::new(false); + +/// Raw PSCI/SMCCC call via the selected conduit. Returns x0. +#[cfg(feature = "smp")] +unsafe fn psci_raw(use_smc: bool, fn_id: u64, a1: u64, a2: u64, a3: u64) -> i64 { + let ret: u64; + if use_smc { + core::arch::asm!( + "smc #0", + inout("x0") fn_id => ret, + in("x1") a1, in("x2") a2, in("x3") a3, + out("x4") _, out("x5") _, out("x6") _, out("x7") _, + options(nostack), + ); + } else { + core::arch::asm!( + "hvc #0", + inout("x0") fn_id => ret, + in("x1") a1, in("x2") a2, in("x3") a3, + out("x4") _, out("x5") _, out("x6") _, out("x7") _, + options(nostack), + ); + } + ret as i64 +} + +/// Probe PSCI_VERSION (0x84000000, side-effect-free) on a conduit. A real PSCI +/// implementation returns a small positive version word (e.g. 0x10001 for v1.1); +/// a conduit that isn't wired returns -1 (NOT_SUPPORTED) or garbage. +#[cfg(feature = "smp")] +unsafe fn psci_version(use_smc: bool) -> i64 { + psci_raw(use_smc, 0x8400_0000, 0, 0, 0) +} + +/// Issue PSCI CPU_ON (SMC64 fn 0xC400_0003) on the selected conduit. +#[cfg(feature = "smp")] +unsafe fn psci_cpu_on(target_mpidr: u64, entry_pa: u64, context_id: u64) -> i64 { + psci_raw( + PSCI_USE_SMC.load(Ordering::Relaxed), + 0xC400_0003, + target_mpidr, + entry_pa, + context_id, + ) +} + +/// Wake secondary cores into `ap_main` (Milestone 1: → idle). Must run after the +/// BSP's MMU + vectors + GIC are up. Reads the PSCI conduit from the DTB; tries +/// CPU_ON for cores 1..=MAX_APS and stops at the first absent core. +#[cfg(feature = "smp")] +pub fn wake_aps() { + let dtb = crate::arch::aarch64::fdt::dtb_ptr(); + // Determine the PSCI conduit. Prefer the DTB; else probe both with the + // side-effect-free PSCI_VERSION and use whichever the platform answers. + let conduit = unsafe { crate::arch::aarch64::fdt::psci_method(dtb) }; + let use_smc = match conduit { + Some(crate::arch::aarch64::fdt::PsciConduit::Smc) => true, + Some(crate::arch::aarch64::fdt::PsciConduit::Hvc) => false, + None => { + let smc_v = unsafe { psci_version(true) }; + let hvc_v = unsafe { psci_version(false) }; + log::error!("[SMP] PSCI_VERSION probe: smc={:#x} hvc={:#x}", smc_v, hvc_v); + // A valid PSCI version is a small positive word (>=0x10000 for v1.x, + // 0x2 for v0.2); -1 = NOT_SUPPORTED on that conduit. + if smc_v > 0 { + true + } else if hvc_v > 0 { + false + } else { + log::error!("[SMP] neither conduit answers PSCI — staying single-core"); + return; + } + } + }; + PSCI_USE_SMC.store(use_smc, Ordering::Relaxed); + log::error!("[SMP] PSCI conduit = {}", if use_smc { "SMC" } else { "HVC" }); + + capture_boot_state(); + let entry = _ap_trampoline as usize as u64; + log::error!("[SMP] AP trampoline @ {:#x}; waking secondary cores...", entry); + + for idx in 1..=MAX_APS as u64 { + // QEMU virt: core N has MPIDR Aff0 = N (< 16 cores). + let target_mpidr = idx; + let st = unsafe { psci_cpu_on(target_mpidr, entry, idx) }; + if st == 0 || st == -4 { + // SUCCESS or ALREADY_ON — wait briefly for it to mark online. + let mut spun = 0u64; + loop { + if unsafe { (*core::ptr::addr_of!(PER_CPU_DATA))[idx as usize].online.load(Ordering::Acquire) } { + break; + } + spun += 1; + if spun > 200_000_000 { + log::error!("[SMP] core {} CPU_ON ok but never came online", idx); + break; + } + core::hint::spin_loop(); + } + } else { + // -2 INVALID_PARAMS = core absent → stop probing further cores. + log::error!("[SMP] core {} CPU_ON returned {} — no more cores", idx, st); + break; + } + } + log::error!("[SMP] online CPU count = {}", CPU_COUNT.load(Ordering::Acquire)); +} + +/// Wake all APs. Sets up the BSP's per-CPU entry, then (under `--features smp`) +/// brings secondary cores online (Milestone 1: into an idle loop). /// -/// TODO(arm): bring APs online via PSCI `CPU_ON` (SMC/HVC) and have each call -/// `crate::arch::ap_init()` then `crate::sched::ap_start()`. +/// Single-core by default — matching x86. Waking cores only idles them today +/// (`ap_main` parks in `wfi`; co-scheduling doesn't converge the Dart GC +/// safepoint — see [[smp-bringup]]) and risks a boot hang on real hardware if an +/// AP faults in the trampoline and the BSP spins waiting for it to come online. pub fn init(_smp_resp: Option<&'static MpResponse>) { unsafe { let table = &mut *core::ptr::addr_of_mut!(PER_CPU_DATA); @@ -82,10 +437,21 @@ pub fn init(_smp_resp: Option<&'static MpResponse>) { table[0].online.store(true, Ordering::Release); } CPU_COUNT.store(1, Ordering::Release); - log::info!("[SMP] aarch64 single-core scaffold (AP wake via PSCI not implemented)"); + + #[cfg(feature = "smp")] + wake_aps(); + #[cfg(not(feature = "smp"))] + log::info!("[SMP] single-core boot (AP bring-up gated behind the `smp` feature)"); } -/// Broadcast a reschedule IPI to all other online CPUs. -/// -/// TODO(arm): send a GIC SGI to the target CPU interface(s). No-op (single core). -pub fn broadcast_resched_ipi() {} +/// Broadcast a reschedule IPI (GIC SGI) to all other online CPUs, so a thread +/// made runnable on this core gets promptly picked up by the core it's affined +/// to. Guarded on CPU_COUNT > 1: single-core never touches the GIC (this is on +/// the hot `set_state(Running)` path), and the SGI is "all-but-self" so it +/// reaches no one anyway on one core. The receiving core's IRQ handler EOIs it +/// and (once APs schedule, M5) reruns its scheduler. +pub fn broadcast_resched_ipi() { + if CPU_COUNT.load(Ordering::Acquire) > 1 { + crate::arch::aarch64::gic::send_sgi_all_but_self(crate::arch::aarch64::gic::SGI_RESCHED); + } +} diff --git a/kernel/src/arch/x86_64/apic.rs b/kernel/src/arch/x86_64/apic.rs index 8ae3c8b..ab23139 100644 --- a/kernel/src/arch/x86_64/apic.rs +++ b/kernel/src/arch/x86_64/apic.rs @@ -60,7 +60,55 @@ fn print_u64_early(val: u64) { } } +/// Upper bound (in TSC cycles) on any legacy-PIT gate spin. The PIT wait is a +/// fixed ~1–10 ms; this bound is many seconds at any real clock, so it never +/// trips on working hardware but guarantees we cannot spin forever on firmware +/// that does not wire the legacy PIT / speaker gate (e.g. Apple Macs), which +/// otherwise hangs boot at "cortex::starting" with the clock frozen at 0.0s. +const PIT_SPIN_TIMEOUT_TSC: u64 = 2_000_000_000; + +/// TSC frequency (Hz) straight from CPUID — no legacy PIT required. Tries leaf +/// 0x15 (TSC / core-crystal ratio) then leaf 0x16 (processor base MHz). Returns +/// 0 when neither is usable (caller falls back to the bounded PIT calibration). +fn tsc_hz_from_cpuid() -> u64 { + let (max_leaf, _, _, _) = crate::arch::x86_64::cpu::cpuid(0, 0); + // Leaf 0x15: EAX=denominator, EBX=numerator, ECX=core-crystal Hz. + if max_leaf >= 0x15 { + let (denom, numer, crystal, _) = crate::arch::x86_64::cpu::cpuid(0x15, 0); + if denom != 0 && numer != 0 { + if crystal != 0 { + return (crystal as u64) * (numer as u64) / (denom as u64); + } + // Crystal often reads 0 on Skylake/Kaby Lake; the nominal TSC equals + // the processor base frequency reported by leaf 0x16. + if max_leaf >= 0x16 { + let (base_mhz, _, _, _) = crate::arch::x86_64::cpu::cpuid(0x16, 0); + if base_mhz != 0 { + return base_mhz as u64 * 1_000_000; + } + } + } + } + // Leaf 0x16: EAX = processor base frequency in MHz. + if max_leaf >= 0x16 { + let (base_mhz, _, _, _) = crate::arch::x86_64::cpu::cpuid(0x16, 0); + if base_mhz != 0 { + return base_mhz as u64 * 1_000_000; + } + } + 0 +} + fn calibrate_tsc() -> u64 { + // Prefer CPUID — it needs no legacy PIT, so it works on firmware (Macs, some + // UEFI) that does not provide the i8254 the PIT path below relies on. Guard + // against bogus values: QEMU/TCG reports nonsensical CPUID 0x15/0x16 (e.g. + // a few MHz), so accept only a plausible CPU clock (0.2–20 GHz) and otherwise + // fall through to the (bounded) PIT measurement. + let cpuid_hz = tsc_hz_from_cpuid(); + if (200_000_000..=20_000_000_000).contains(&cpuid_hz) { + return cpuid_hz; + } unsafe { let val = super::port_io::inb(0x61); super::port_io::outb(0x61, (val & 0xFD) | 1); @@ -76,16 +124,26 @@ fn calibrate_tsc() -> u64 { super::port_io::outb(0x61, val2 | 1); let start = crate::arch::rdtsc(); + // BOUNDED spin: bail if the PIT gate never fires (missing/idle PIT). while (super::port_io::inb(0x61) & 0x20) == 0 { + if crate::arch::rdtsc().wrapping_sub(start) > PIT_SPIN_TIMEOUT_TSC { + break; + } core::hint::spin_loop(); } let end = crate::arch::rdtsc(); + let fired = (super::port_io::inb(0x61) & 0x20) != 0; let elapsed = end.wrapping_sub(start); - + let val3 = super::port_io::inb(0x61); super::port_io::outb(0x61, val3 & 0xFC); + // PIT never fired within the bound → no usable measurement. Return 0 so + // the caller uses the conservative default instead of a bogus value. + if !fired { + return 0; + } elapsed * 100 } } @@ -117,7 +175,14 @@ fn calibrate_apic_ticks_per_ms() -> u32 { } } + // BOUNDED spin (see PIT_SPIN_TIMEOUT_TSC): a missing/idle PIT must not + // hang boot. If it never fires, `current` stays the full count → ticks=0 + // → the caller's 62500 default is used. + let spin_start = crate::arch::rdtsc(); while (super::port_io::inb(0x61) & 0x20) == 0 { + if crate::arch::rdtsc().wrapping_sub(spin_start) > PIT_SPIN_TIMEOUT_TSC { + break; + } core::hint::spin_loop(); } @@ -331,7 +396,12 @@ pub fn finish_xapic_init() { } // Calibrate TSC. - let tsc_hz = calibrate_tsc(); + // 0 means neither CPUID nor a working PIT gave a value — keep the safe 2 GHz + // default rather than setting a 0 frequency (which would zero the timer period). + let tsc_hz = match calibrate_tsc() { + 0 => 2_000_000_000, + hz => hz, + }; set_tsc_hz(tsc_hz); crate::logger::early_print("[APIC] Calibrated TSC frequency: "); print_u64_early(tsc_hz); diff --git a/kernel/src/arch/x86_64/idt.rs b/kernel/src/arch/x86_64/idt.rs index ef175f6..46adc5f 100644 --- a/kernel/src/arch/x86_64/idt.rs +++ b/kernel/src/arch/x86_64/idt.rs @@ -67,7 +67,7 @@ pub fn init() { IDT.entries[6].set(invalid_opcode_handler as *const () as u64, 0, 0); IDT.entries[7].set(device_not_avail_handler as *const () as u64, 0, 0); IDT.entries[8].set(double_fault_handler as *const () as u64, DOUBLE_FAULT_IST, 0); - IDT.entries[13].set(general_protection_handler as *const () as u64, 0, 0); + IDT.entries[13].set(gp_fault_entry as *const () as u64, 0, 0); IDT.entries[14].set(page_fault_entry as *const () as u64, 0, 0); IDT.entries[16].set(x87_fp_handler as *const () as u64, 0, 0); IDT.entries[17].set(alignment_check_handler as *const () as u64, 0, 0); @@ -131,29 +131,72 @@ extern "x86-interrupt" fn device_not_avail_handler(_frame: InterruptFrame) {} extern "x86-interrupt" fn double_fault_handler(frame: InterruptFrame, _err: u64) -> ! { panic!("Double fault! ip={:#x}", frame.ip); } -extern "x86-interrupt" fn general_protection_handler(frame: InterruptFrame, err: u64) { - crate::cortex::interrupt_hook(13, &frame, Some(err)); - let user_mode = (frame.cs & 0x3) == 0x3; +// #GP frame: same layout as PageFaultFrame (the CPU pushes an error code for #GP too). +#[repr(C)] +struct GpFaultFrame { + r15: u64, r14: u64, r13: u64, r12: u64, + r11: u64, r10: u64, r9: u64, r8: u64, + rdi: u64, rsi: u64, rbp: u64, rbx: u64, + rdx: u64, rcx: u64, rax: u64, + err: u64, + rip: u64, cs: u64, rflags: u64, rsp: u64, ss: u64, +} + +#[unsafe(naked)] +unsafe extern "C" fn gp_fault_entry() { + core::arch::naked_asm!( + "push rax", "push rcx", "push rdx", "push rbx", "push rbp", + "push rsi", "push rdi", "push r8", "push r9", "push r10", + "push r11", "push r12", "push r13", "push r14", "push r15", + "mov rdi, rsp", + "call {handler}", + "pop r15", "pop r14", "pop r13", "pop r12", "pop r11", + "pop r10", "pop r9", "pop r8", "pop rdi", "pop rsi", + "pop rbp", "pop rbx", "pop rdx", "pop rcx", "pop rax", + "add rsp, 8", + "iretq", + handler = sym gp_fault_full_handler, + ); +} + +extern "C" fn gp_fault_full_handler(frame_ptr: *mut GpFaultFrame) { + let f = unsafe { &*frame_ptr }; + let user_mode = (f.cs & 0x3) == 0x3; if user_mode { let pid = crate::process::current_pid(); log::error!( "[GPF] SIGSEGV: ip={:#x} err={:#x} pid={} cs={:#x} sp={:#x} ss={:#x} rflags={:#x}", - frame.ip, - err, - pid, - frame.cs, - frame.sp, - frame.ss, - frame.flags + f.rip, f.err, pid, f.cs, f.rsp, f.ss, f.rflags + ); + log::error!( + "[GPF] regs: rax={:#x} rbx={:#x} rcx={:#x} rdx={:#x} rsi={:#x} rdi={:#x} rbp={:#x}", + f.rax, f.rbx, f.rcx, f.rdx, f.rsi, f.rdi, f.rbp + ); + log::error!( + "[GPF] regs: r8={:#x} r9={:#x} r10={:#x} r11={:#x} r12={:#x} r13={:#x} r14={:#x} r15={:#x}", + f.r8, f.r9, f.r10, f.r11, f.r12, f.r13, f.r14, f.r15 ); crate::syscall::dump_recent_syscalls(24); + // RESILIENCE: a userspace fault must NOT halt the whole OS (the old + // `halt_forever` turned one app's crash into a dead machine). A user-mode + // fault holds no kernel lock, so we can safely tear down the faulting + // process — `exit` triggers app crash auto-recovery for the app's group — + // and reschedule. The shell and every other process survive one app's + // crash. Mirrors sys_exit: die, run the next runnable, never park forever. if pid != 0 { - crate::process::kill(pid).ok(); + crate::process::exit(pid, -1); } crate::process::set_current_pid(0); - crate::arch::halt_forever(); + loop { + crate::process::handle_pending_wakes_try(); + crate::syscall::check_timerfds_and_wake_try(); + if let Some(next) = crate::process::next_runnable_pid(0) { + crate::process::enter_user_by_pid_noreturn(next); + } + crate::arch::enable_and_halt(); + } } - panic!("GPF! ip={:#x} err={:#x}", frame.ip, err); + panic!("GPF! ip={:#x} err={:#x}", f.rip, f.err); } extern "x86-interrupt" fn page_fault_handler(frame: InterruptFrame, err: u64) { // Kept for any vestigial callers; primary handler is page_fault_entry below. @@ -382,11 +425,21 @@ extern "C" fn page_fault_full_handler(frame_ptr: *mut PageFaultFrame) { } crate::syscall::dump_recent_syscalls(24); if user_mode { + // RESILIENCE (matches the #GP handler): a userspace page fault kills only + // the faulting process (triggering app crash auto-recovery) and reschedules + // — it must not halt the whole OS. A user-mode fault holds no kernel lock. if pid != 0 { - crate::process::kill(pid).ok(); + crate::process::exit(pid, -1); } crate::process::set_current_pid(0); - crate::arch::halt_forever(); + loop { + crate::process::handle_pending_wakes_try(); + crate::syscall::check_timerfds_and_wake_try(); + if let Some(next) = crate::process::next_runnable_pid(0) { + crate::process::enter_user_by_pid_noreturn(next); + } + crate::arch::enable_and_halt(); + } } panic!("Unresolvable kernel page fault: addr={:#x} err={:#x} ip={:#x}", cr2, frame.err, frame.rip); } @@ -574,16 +627,27 @@ unsafe extern "C" fn apic_timer_entry() { } extern "C" fn apic_timer_handler(frame_ptr: *mut TimerTrapFrame) { + let cpu_id = crate::arch::x86_64::smp::this_cpu().cpu_id; + + // AP idle-park during shell bring-up: until the shell's engine is up (and thus + // an app could be launched onto an AP), an AP has no home-pinned work. Doing the + // timerfd/wake/scheduler work here only contends with the BSP's first-frame + // bring-up (a boot stall under TCG SMP). EOI and return — the AP just halts. + if cpu_id != 0 && !crate::wm::flutter_init_ready() { + crate::arch::apic::eoi(); + return; + } + crate::syscall::check_timerfds_and_wake_try(); crate::process::handle_pending_wakes_try(); - let cpu_id = crate::arch::x86_64::smp::this_cpu().cpu_id; - // Set a deferred-kick flag every ~500 ms (500 ticks × 1 ms/tick). // The flag is consumed in syscall context (sys_pthread_mutex_unlock or // sys_wm_event_wait) where spinlock acquisition is safe. This ISR path - // only does an atomic store — completely lock-free. - if cpu_id == 0 { + // only does an atomic store — completely lock-free. Fired on ALL cores: a + // launched app runs on an AP and needs its own cooperative deadlocks broken + // (the global flag is consumed by whichever core's thread next yields). + { static TR_KICK_CTR: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0); let k = TR_KICK_CTR.fetch_add(1, core::sync::atomic::Ordering::Relaxed); @@ -678,35 +742,36 @@ extern "C" fn apic_timer_handler(frame_ptr: *mut TimerTrapFrame) { return; } - // SMP SAFETY: the scheduler is cooperative and BSP-only — it assumes at most - // ONE user thread runs at a time. Only CPU 0 may enter/run user threads. If an - // AP's timer ISR were allowed past here it would claim and enter a user thread - // (via the wake-assist below), running it CONCURRENTLY with the BSP. Two user - // threads executing in true parallel break the futex/condvar emulation's - // single-core assumptions → nondeterministic livelock (the smp>1 failure). - // Keep APs out: they EOI'd above and now simply return to halt. - if cpu_id != 0 { - return; - } - - // Kernel-mode wake assist: when a Flutter runner is sleeping in a syscall - // hlt loop, queued input can otherwise leave PID 1 runnable-but-unentered. - // If the interrupt lands in idle (cur=0), PID 1 is still the safe target - // for post-init WM input. If it lands in PID 1's own wm_event_wait loop, - // re-entering PID 1 makes it re-execute the syscall and drain the queue. + // SMP home-core model: each core runs ONLY threads pinned to its home_cpu, so + // the BSP and an AP never enter the SAME user thread concurrently. The old + // "BSP-only" gate (which parked every AP) is replaced by a per-target home-core + // check inside try_claim_cpu_for_try below: a core's wake-assist can only claim + // and enter a target whose home_cpu == this core. The shell (home 0) is woken by + // the BSP; a launched app (home 1+) is woken by its AP. On single-core there is + // only cpu 0 and home_cpu is always 0, so this is exactly the original path. + // + // Kernel-mode wake assist: when a Flutter runner is sleeping in a syscall hlt + // loop, queued input/baton can leave it runnable-but-unentered. Re-entering the + // target makes it re-execute its wm_event_wait and drain the queue (pumping a + // frame). This is what pumps a launched app's frames on its application core. + let my_cpu = cpu_id; let cur = crate::process::current_pid(); let focus = crate::wm::focus_pid(); let target = if focus != 0 { focus } else { 1 }; - if target != 0 + // Only the target's HOME core runs the wake-assist for it. This guards the WHOLE + // block (not just the claim): set_state_try has a side effect, so an AP must not + // flip the shell's state to Running from core 1 (that races the shell's own + // cooperative state machine on core 0 — observed to freeze the shell's bring-up). + // On single-core, home_cpu is always 0 == my_cpu, so this is the original path. + let target_is_mine = crate::process::home_cpu_of_try(target) == Some(my_cpu); + if target != 0 && target_is_mine && crate::wm::flutter_init_ready() && !crate::wm::flutter_bootstrap_spin_active() && (cur == 0 || crate::process::is_blocked_try(cur)) && (crate::wm::input_pending_for(target) > 0 || crate::wm::embedder_baton_due(target)) { if crate::process::set_state_try(target, crate::process::ProcState::Running) { - if cur != target { - let my_cpu = crate::arch::smp::this_cpu().cpu_id; if crate::process::try_claim_cpu_for_try(target, my_cpu) { crate::process::enter_user_by_pid_noreturn_try(target); } diff --git a/kernel/src/arch/x86_64/smp.rs b/kernel/src/arch/x86_64/smp.rs index fa3ef93..2a2fb2e 100644 --- a/kernel/src/arch/x86_64/smp.rs +++ b/kernel/src/arch/x86_64/smp.rs @@ -25,6 +25,21 @@ use limine::request::MpResponse; /// Maximum CPUs supported. pub const MAX_CPUS: usize = 64; +/// Gate: true once CPU 0 has entered the scheduler and claimed the init thread. +/// APs must not run the scheduler before this (else they'd race the BSP to claim +/// the init thread). Set inside `enter_user_by_pid_noreturn` on CPU 0. Mirrors +/// the aarch64 SMP gate so the arch-neutral scheduler code links on both arches. +pub static SMP_SCHED_READY: AtomicBool = AtomicBool::new(false); + +#[inline] +pub fn sched_ready() -> bool { + SMP_SCHED_READY.load(Ordering::Acquire) +} +#[inline] +pub fn set_sched_ready() { + SMP_SCHED_READY.store(true, Ordering::Release); +} + // ── Per-CPU data ────────────────────────────────────────────────────────────── /// Per-CPU mutable state — one entry per logical processor. @@ -102,56 +117,83 @@ pub fn init(smp_resp: Option<&'static MpResponse>) { PER_CPU_DATA[0].online.store(true, Ordering::Release); } - let resp = match smp_resp { - Some(r) => r, - None => { - log::warn!("[SMP] Limine SMP response missing — single-core only"); - return; - } - }; - - let cpus = resp.cpus(); - let ap_count = cpus.iter().filter(|c| c.lapic_id != bsp_lapic_id).count(); - if ap_count == 0 { - log::info!("[SMP] No APs — single-core"); - return; + // SMP scheduling isn't verified safe on real hardware yet: the scheduler is + // not fully SMP-safe, ap_init faulted on a real 2016 Skylake Mac, and the BSP + // then ground through a spin-count AP timeout (PAUSE is ~140 cycles on + // Skylake, so the old 200x5M budget was ~minutes per AP) — hanging the boot at + // cortex::smp. Single-core is the proven-stable config (the freeze is fixed by + // the serial-GC engine, not SMP). Boot single-core by default; build with + // `--features smp` to develop AP bring-up. + #[cfg(not(feature = "smp"))] + { + let _ = smp_resp; + log::info!("[SMP] single-core boot (AP bring-up gated behind the `smp` feature)"); } - log::info!("[SMP] {} total CPU(s) — waking {} AP(s)", cpus.len(), ap_count); - - let mut cpu_idx: u32 = 1; - for cpu in cpus { - if cpu.lapic_id == bsp_lapic_id { continue; } - if cpu_idx as usize >= MAX_CPUS { - log::warn!("[SMP] MAX_CPUS limit reached — skipping remaining APs"); - break; + + #[cfg(feature = "smp")] + { + let resp = match smp_resp { + Some(r) => r, + None => { + log::warn!("[SMP] Limine SMP response missing — single-core only"); + return; + } + }; + + let cpus = resp.cpus(); + let ap_count = cpus.iter().filter(|c| c.lapic_id != bsp_lapic_id).count(); + if ap_count == 0 { + log::info!("[SMP] No APs — single-core"); + return; } - unsafe { - PER_CPU_DATA[cpu_idx as usize].cpu_id = cpu_idx; - PER_CPU_DATA[cpu_idx as usize].lapic_id = cpu.lapic_id; + log::info!("[SMP] {} total CPU(s) — waking {} AP(s)", cpus.len(), ap_count); + + let mut cpu_idx: u32 = 1; + for cpu in cpus { + if cpu.lapic_id == bsp_lapic_id { continue; } + if cpu_idx as usize >= MAX_CPUS { + log::warn!("[SMP] MAX_CPUS limit reached — skipping remaining APs"); + break; + } + unsafe { + PER_CPU_DATA[cpu_idx as usize].cpu_id = cpu_idx; + PER_CPU_DATA[cpu_idx as usize].lapic_id = cpu.lapic_id; + } + CPU_COUNT.store(cpu_idx + 1, Ordering::Release); + cpu.bootstrap(ap_entry as MpGotoFunction, cpu_idx as u64); + cpu_idx += 1; } - CPU_COUNT.store(cpu_idx + 1, Ordering::Release); - cpu.bootstrap(ap_entry as MpGotoFunction, cpu_idx as u64); - cpu_idx += 1; - } - // Wait up to ~2 s per AP to come online. - let expected = cpu_idx as usize; - 'outer: for i in 1..expected { - for _ in 0..200u32 { - if unsafe { PER_CPU_DATA[i].online.load(Ordering::Acquire) } { - continue 'outer; + // Wait for each AP to come online — bounded in WALL-CLOCK time via the + // monotonic clock, NOT a spin count (PAUSE is ~140 cycles on Skylake, so a + // spin budget is ~minutes per AP on real hardware when an AP fails to start). + let expected = cpu_idx as usize; + const AP_TIMEOUT_NS: u64 = 500_000_000; // 0.5 s per AP, then continue + for i in 1..expected { + let deadline = crate::arch::rdtsc_ns().wrapping_add(AP_TIMEOUT_NS); + while !unsafe { PER_CPU_DATA[i].online.load(Ordering::Acquire) } { + if crate::arch::rdtsc_ns() >= deadline { + log::warn!("[SMP] CPU {} (lapic={}) timed out — continuing", + i, unsafe { PER_CPU_DATA[i].lapic_id }); + break; + } + core::hint::spin_loop(); } - for _ in 0..5_000_000usize { core::hint::spin_loop(); } } - log::warn!("[SMP] CPU {} (lapic={}) timed out", - i, unsafe { PER_CPU_DATA[i].lapic_id }); - } - CPU_COUNT.store(cpu_idx, Ordering::Release); - let online = (0..expected) - .filter(|&i| unsafe { PER_CPU_DATA[i].online.load(Ordering::Acquire) }) - .count(); - log::info!("[SMP] {}/{} CPU(s) online", online, expected); + // Publish the ONLINE count, not the configured count. The scheduler reads + // `CPU_COUNT > 1` to decide whether to take the home-pinned SMP path, and + // `pick_home_cpu_for_app` pins apps to cores `1..CPU_COUNT`. If an AP failed + // to come online we MUST NOT advertise it — otherwise an app pinned to that + // dead core would never be scheduled (starves at a black screen). APs come + // online contiguously from index 1 under Limine, so the online count is also + // a valid scan range for `current_cpu_id()`. + let online = (0..expected) + .filter(|&i| unsafe { PER_CPU_DATA[i].online.load(Ordering::Acquire) }) + .count(); + CPU_COUNT.store(online as u32, Ordering::Release); + log::error!("[SMP] {}/{} CPU(s) online — CPU_COUNT={}", online, expected, online); + } } /// Broadcast a reschedule IPI to all other online CPUs. diff --git a/kernel/src/compositor/input_hud.rs b/kernel/src/compositor/input_hud.rs new file mode 100644 index 0000000..21b8dd9 --- /dev/null +++ b/kernel/src/compositor/input_hud.rs @@ -0,0 +1,134 @@ +//! On-screen input debug HUD — opt-in via `--features input-hud`. +//! +//! Draws a compact panel each frame showing which input device bound and the +//! live pointer/key/scroll event counts plus the last pointer position. This is +//! the "eyes on it" tool for diagnosing input on a real VM or board where there +//! is no serial console: +//! * `src=none` → no input device bound at all. +//! * `p=` never rises → events aren't reaching the kernel while you move/click. +//! * `xy=` pinned off-screen → the cursor is being clamped or scaled wrong. +//! * border red → no device; cyan → a device is bound. +//! +//! Release builds (without the feature) compile `draw()` to a no-op, so the +//! shipped ISOs stay clean and pay zero cost. + +/// Draw the input HUD over the current back buffer (call just before swap). +#[cfg(feature = "input-hud")] +pub fn draw() { + use crate::drivers::fb; + let (_sw, sh) = match fb::size_px() { + Some(s) => s, + None => return, + }; + + let (pc, kc, scnt) = crate::wm::input_counts(); + let (cx, cy) = crate::wm::cursor_pos(); + let btn = crate::wm::cursor_buttons(); + let (key, pressed) = crate::wm::last_key(); + let src = crate::wm::input_source_name(); + let bound = crate::wm::input_source() != crate::wm::INPUT_SRC_NONE; + + // line 1: device + per-kind event counters + let mut l1 = Line::new(); + l1.s(b"INPUT src="); + l1.s(src.as_bytes()); + l1.s(b" p="); + l1.u(pc as u64); + l1.s(b" k="); + l1.u(kc as u64); + l1.s(b" s="); + l1.u(scnt as u64); + + // line 2: last pointer position / buttons / key + let mut l2 = Line::new(); + l2.s(b"xy="); + l2.i(cx); + l2.b(b','); + l2.i(cy); + l2.s(b" btn="); + l2.u(btn as u64); + l2.s(b" key=0x"); + l2.hex(key as u8); + l2.b(if pressed { b'+' } else { b'-' }); + + let scale = 2u32; + let line_h = (8 * scale + 3) as i32; + let pad = 6i32; + let tw = fb::text_width(l1.len, scale).max(fb::text_width(l2.len, scale)); + let panel_w = (tw + pad * 2) as u32; + let panel_h = (line_h * 2 + pad * 2) as u32; + let x0 = 8i32; + let y0 = sh as i32 - panel_h as i32 - 8; + + fb::fill_rect(x0, y0, panel_w, panel_h, 0x00000000); + let border = if bound { 0x0038BDF8u32 } else { 0x00FF3030u32 }; + fb::fill_rect(x0, y0, panel_w, 2, border); + fb::fill_rect(x0, y0 + panel_h as i32 - 2, panel_w, 2, border); + + let tx = x0 + pad; + fb::draw_text(l1.as_slice(), tx, y0 + pad, scale, 0x0038BDF8); + fb::draw_text(l2.as_slice(), tx, y0 + pad + line_h, scale, 0x00FFFFFF); +} + +/// No-op when the HUD feature is disabled (clean release builds). +#[cfg(not(feature = "input-hud"))] +#[inline(always)] +pub fn draw() {} + +/// Fixed-capacity ASCII line builder — no heap, no `core::fmt` formatting cost. +#[cfg(feature = "input-hud")] +struct Line { + buf: [u8; 96], + len: usize, +} + +#[cfg(feature = "input-hud")] +impl Line { + fn new() -> Self { + Self { buf: [0; 96], len: 0 } + } + fn as_slice(&self) -> &[u8] { + &self.buf[..self.len] + } + fn b(&mut self, c: u8) { + if self.len < self.buf.len() { + self.buf[self.len] = c; + self.len += 1; + } + } + fn s(&mut self, s: &[u8]) { + for &c in s { + self.b(c); + } + } + fn u(&mut self, mut n: u64) { + if n == 0 { + self.b(b'0'); + return; + } + let mut tmp = [0u8; 20]; + let mut i = 0; + while n > 0 { + tmp[i] = b'0' + (n % 10) as u8; + n /= 10; + i += 1; + } + while i > 0 { + i -= 1; + self.b(tmp[i]); + } + } + fn i(&mut self, n: i32) { + if n < 0 { + self.b(b'-'); + self.u((-(n as i64)) as u64); + } else { + self.u(n as u64); + } + } + fn hex(&mut self, n: u8) { + let h = b"0123456789ABCDEF"; + self.b(h[(n >> 4) as usize]); + self.b(h[(n & 0xF) as usize]); + } +} diff --git a/kernel/src/compositor/mod.rs b/kernel/src/compositor/mod.rs index 47c276a..eaf18ff 100644 --- a/kernel/src/compositor/mod.rs +++ b/kernel/src/compositor/mod.rs @@ -9,6 +9,9 @@ use alloc::vec::Vec; use spin::Mutex; use core::sync::atomic::{AtomicBool, AtomicU32, Ordering}; +/// On-screen input debug HUD (opt-in via `--features input-hud`; no-op otherwise). +mod input_hud; + /// The mouse-cursor shape the compositor currently draws. Set from userspace /// via `SYS_CURSOR_SHAPE_SET` when the Flutter framework activates a system /// cursor (e.g. hovering a button → click/hand, hovering a text field → I-beam). @@ -954,6 +957,7 @@ pub fn render_frame() { // verbose log overlay) instead of raw logs. It fills its own black bg. crate::drivers::bootscreen::render(); draw_software_cursor(); + input_hud::draw(); crate::drivers::fb::swap_buffers(); crate::wm::push_vsync(frame); return; @@ -1020,6 +1024,7 @@ pub fn render_frame() { drop(c); draw_software_cursor(); + input_hud::draw(); crate::drivers::fb::swap_buffers(); let mut c = COMP.lock(); c.frame_counter = c.frame_counter.wrapping_add(1); diff --git a/kernel/src/drivers/bootscreen.rs b/kernel/src/drivers/bootscreen.rs index 08a1dc5..09d57fd 100644 --- a/kernel/src/drivers/bootscreen.rs +++ b/kernel/src/drivers/bootscreen.rs @@ -11,7 +11,7 @@ //! serial console for developers). Press **F2** to toggle a verbose overlay that //! renders the recent kernel log ring on demand. -use core::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +use core::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering}; use crate::drivers::fb; use crate::logger; @@ -29,6 +29,16 @@ const LOG_ERR: u32 = 0x00E0_5A4A; // verbose log error/warn text static START_NS: AtomicU64 = AtomicU64::new(0); static VERBOSE: AtomicBool = AtomicBool::new(false); static DONE: AtomicBool = AtomicBool::new(false); +/// Explicit boot milestone (0 = none → fall back to the time-derived stage). +/// Set by `set_phase` at each major init step so the on-screen status names the +/// LAST step reached — when boot wedges (e.g. on real hardware QEMU can't +/// reproduce), the frozen splash shows exactly where it stopped. +static PHASE: AtomicU32 = AtomicU32::new(0); + +/// Record the current boot milestone (see PHASE). Phases are listed in `stage()`. +pub fn set_phase(p: u32) { + PHASE.store(p, Ordering::Release); +} fn now_ns() -> u64 { crate::syscall::poll::monotonic_ns() @@ -87,9 +97,25 @@ fn stage() -> &'static [u8] { if DONE.load(Ordering::Acquire) { return b"boot_completed"; } - match percent() { - 0..=24 => b"starting", - 25..=54 => b"init_runtime", + // Explicit milestone wins (set by set_phase at each init step). The frozen + // splash then names the last step reached if boot wedges on real hardware. + match PHASE.load(Ordering::Acquire) { + 0 => match percent() { + 0..=24 => b"starting", + 25..=54 => b"init_runtime", + _ => b"warming_engine", + }, + 1 => b"drivers", + 2 => b"compositor", + 3 => b"scheduler", + 4 => b"ipc_wm", + 5 => b"filesystem", + 6 => b"block_net", + 7 => b"packages", + 8 => b"cortex", + 9 => b"smp", + 10 => b"spawn_shell", + 11 => b"enter_user", _ => b"warming_engine", } } diff --git a/kernel/src/drivers/fb.rs b/kernel/src/drivers/fb.rs index 9483004..8d51e58 100644 --- a/kernel/src/drivers/fb.rs +++ b/kernel/src/drivers/fb.rs @@ -626,7 +626,25 @@ pub fn set_double_buffer(active: bool) { let height = FB_HEIGHT.load(Ordering::Relaxed); let pitch_px = FB_PITCH_PX.load(Ordering::Relaxed); let size = pitch_px as usize * height as usize; - *db = Some(vec![0u32; size]); + // A full-fb back buffer is large on high-res panels — a 2880x1800 + // Retina framebuffer (e.g. a 2016 MacBook Pro) is ~20 MiB. Allocate + // it FALLIBLY (and reject an implausible geometry) so a big fb or a + // tight heap degrades to single-buffer / direct-to-fb rendering + // instead of `vec!` aborting and hanging the boot at + // cortex::compositor. Single-buffer is fully supported (every write + // path checks DOUBLE_BUFFER_ACTIVE); it just tears. + const MAX_DB_PIXELS: usize = 24 * 1024 * 1024; // ~96 MiB sanity ceiling + let mut buf: Vec = Vec::new(); + if size == 0 || size > MAX_DB_PIXELS || buf.try_reserve_exact(size).is_err() { + DOUBLE_BUFFER_ACTIVE.store(false, Ordering::SeqCst); + log::error!( + "[fb] double-buffer unavailable (size={} px) — single-buffer mode", + size + ); + } else { + buf.resize(size, 0u32); + *db = Some(buf); + } } } } diff --git a/kernel/src/drivers/ps2.rs b/kernel/src/drivers/ps2.rs index 9559cca..1f2d009 100644 --- a/kernel/src/drivers/ps2.rs +++ b/kernel/src/drivers/ps2.rs @@ -359,8 +359,21 @@ fn handle_mouse_byte(byte: u8) { /// IDT vectors are registered (i.e., after `idt::init()`). pub fn init() { unsafe { - // Flush output buffer. - while in8(PS2_STATUS) & STATUS_OBF != 0 { + // Absent i8042 (firmware without a legacy PS/2 controller — e.g. some + // UEFI/UTM profiles) reads 0xFF on the status port. 0xFF & STATUS_OBF is + // always set, so the flush loops below would spin forever. Detect the + // open-bus 0xFF and skip PS/2 entirely (leaving PS2_READY false → the WM + // falls back to other input). This is the i8042 analogue of the legacy + // PIT hang. + if in8(PS2_STATUS) == 0xFF { + log::warn!("[PS2] no i8042 controller (status=0xFF) — skipping PS/2 init"); + return; + } + + // Flush output buffer (BOUNDED: a stuck-OBF / open-bus controller must + // not spin forever; a real i8042 has at most a couple of bytes queued). + for _ in 0..4096 { + if in8(PS2_STATUS) & STATUS_OBF == 0 { break; } let _ = in8(PS2_DATA); } @@ -368,8 +381,9 @@ pub fn init() { wait_write(); out8(PS2_STATUS, 0xAD); // disable port 1 wait_write(); out8(PS2_STATUS, 0xA7); // disable port 2 - // Flush again after disable. - while in8(PS2_STATUS) & STATUS_OBF != 0 { + // Flush again after disable (bounded, same rationale). + for _ in 0..4096 { + if in8(PS2_STATUS) & STATUS_OBF == 0 { break; } let _ = in8(PS2_DATA); } @@ -411,6 +425,7 @@ pub fn init() { PS2_DEVICES.store(devs, Ordering::Release); LAST_ACTIVITY_TSC.store(crate::arch::rdtsc(), Ordering::Relaxed); PS2_READY.store(true, Ordering::Release); + crate::wm::set_input_source(crate::wm::INPUT_SRC_PS2); } log::info!( diff --git a/kernel/src/drivers/virtio_input.rs b/kernel/src/drivers/virtio_input.rs index 88df836..e2853f4 100644 --- a/kernel/src/drivers/virtio_input.rs +++ b/kernel/src/drivers/virtio_input.rs @@ -211,6 +211,7 @@ pub fn init() { status |= S_DRIVER_OK; w32(base, STATUS, status); + crate::wm::set_input_source(crate::wm::INPUT_SRC_VIRTIO); // Enable this slot's SPI at the GIC (level-triggered, routed to CPU0). let intid = SPI_BASE_INTID + slot as u32; diff --git a/kernel/src/drivers/xhci_runtime.rs b/kernel/src/drivers/xhci_runtime.rs index 0e3de2f..0241474 100644 --- a/kernel/src/drivers/xhci_runtime.rs +++ b/kernel/src/drivers/xhci_runtime.rs @@ -147,6 +147,11 @@ pub struct XhciRuntime { last_hid: [u8; HID_LEN], cur_x: i32, cur_y: i32, + // True when the HID interface is an ABSOLUTE pointer (USB tablet) rather than + // a boot keyboard/mouse. UTM/QEMU's default pointer is `usb-tablet`, which has + // bInterfaceProtocol=0 (no boot protocol) and reports absolute X/Y — without + // this it was misrouted to the boot-keyboard handler (no pointer in UTM). + hid_is_tablet: bool, } static RUNTIME_OK: AtomicBool = AtomicBool::new(false); @@ -333,6 +338,7 @@ pub fn start(ctrl: &XhciController) -> Result<(), &'static str> { last_hid: [0; HID_LEN], cur_x: 64, cur_y: 64, + hid_is_tablet: false, }); RUNTIME_OK.store(true, Ordering::Release); log::info!("[USB] XHCI runtime started"); @@ -343,6 +349,7 @@ pub fn start(ctrl: &XhciController) -> Result<(), &'static str> { if try_enumerate(rt) { rt.enumerated = true; log::info!("[USB-HID] device enumerated slot={} protocol={}", rt.slot, rt.hid_protocol); + crate::wm::set_input_source(crate::wm::INPUT_SRC_XHCI); arm_hid_transfer(rt); break; } @@ -546,6 +553,13 @@ fn route_report(report: &[u8], rt: &mut XhciRuntime) { let old = rt.last_hid; rt.last_hid.copy_from_slice(report); + // Absolute pointer (USB tablet, e.g. UTM's default): parse abs X/Y. Deduped + // above (an unchanged abs report is the same position, safe to skip). + if rt.hid_is_tablet { + route_tablet(report, rt); + return; + } + if rt.hid_protocol == 1 || rt.hid_protocol == 0 { if let Some((sc, pressed)) = usb_hid::handle_boot_keyboard_report(report) { crate::wm::push_key(sc, pressed); @@ -581,6 +595,27 @@ fn route_mouse(report: &[u8], rt: &mut XhciRuntime) { } } +/// USB tablet (absolute) report: byte0 = buttons (b0 L, b1 R, b2 M), bytes1-2 = +/// absolute X (u16 LE, 0..=32767), bytes3-4 = absolute Y, byte5 = wheel (i8). +/// This is QEMU/UTM's `usb-tablet` format. Map the absolute range onto the +/// framebuffer and feed the WM's unified pointer state. +fn route_tablet(report: &[u8], rt: &mut XhciRuntime) { + if report.len() < 5 { + return; + } + let buttons = (report[0] as u32) & 0x7; + let ax = u16::from_le_bytes([report[1], report[2]]) as u32; + let ay = u16::from_le_bytes([report[3], report[4]]) as u32; + let (w, h) = crate::drivers::fb::size_px().unwrap_or((1024, 768)); + rt.cur_x = ((ax * w.saturating_sub(1)) / 32767) as i32; + rt.cur_y = ((ay * h.saturating_sub(1)) / 32767) as i32; + crate::wm::push_pointer(rt.cur_x, rt.cur_y, buttons); + let wheel = report.get(5).map(|b| *b as i8 as i32).unwrap_or(0); + if wheel != 0 { + crate::wm::push_scroll(rt.cur_x, rt.cur_y, wheel); + } +} + unsafe fn write_input_address_ctx(rt: &XhciRuntime, port: u8, speed: u32) { let p = p2v(rt.in_phys) as *mut u8; core::ptr::write_bytes(p, 0, 512); @@ -735,13 +770,66 @@ unsafe fn try_enumerate(rt: &mut XhciRuntime) -> bool { return false; } + // For a non-boot HID interface (bInterfaceProtocol=0), read the HID report + // descriptor to classify it: UTM/QEMU's default pointer is `usb-tablet`, + // which is protocol 0 and reports ABSOLUTE X/Y. Without this it falls through + // to the boot-keyboard handler and the pointer never moves. + if rt.hid_protocol == 0 { + // GET_DESCRIPTOR(HID Report = 0x22, index 0) to interface 0. + if control_xfer(rt, 0x81, 0x06, 0x2200, 0, 128, rt.ctrl_buf_phys) { + let rd = core::slice::from_raw_parts(p2v(rt.ctrl_buf_phys) as *const u8, 128); + rt.hid_is_tablet = report_desc_is_pointer(rd); + log::info!( + "[USB-HID] protocol=0 → {}", + if rt.hid_is_tablet { "absolute pointer (tablet)" } else { "keyboard/other" } + ); + } + } + // HID class requests: SET_PROTOCOL(boot=0) so reports are boot-format, and - // SET_IDLE(0) so the device only reports on change. Best-effort. + // SET_IDLE(0) so the device only reports on change. Best-effort. (A tablet has + // no boot protocol; SET_PROTOCOL is a harmless no-op for it.) let _ = control_xfer(rt, 0x21, 0x0B, 0, 0, 0, 0); // SET_PROTOCOL boot let _ = control_xfer(rt, 0x21, 0x0A, 0, 0, 0, 0); // SET_IDLE infinite true } +/// Scan a HID report descriptor for a top-level Generic-Desktop Mouse/Pointer +/// usage → absolute/relative pointer device (e.g. a USB tablet). Minimal item +/// walk: item byte = bTag(4)|bType(2)|bSize(2); 0x04 = Usage Page (global), +/// 0x08 = Usage (local). Generic Desktop page = 0x01; Mouse = 0x02, Pointer = +/// 0x01, Keyboard = 0x06. +fn report_desc_is_pointer(rd: &[u8]) -> bool { + let mut generic_desktop = false; + let mut i = 0usize; + while i < rd.len() { + let b = rd[i]; + if b == 0 { + break; + } + let dlen = match b & 0x03 { + 3 => 4, + n => n as usize, + }; + let tag = b & 0xFC; + let data = if dlen >= 1 { rd.get(i + 1).copied().unwrap_or(0) } else { 0 }; + if tag == 0x04 { + // Usage Page + generic_desktop = data == 0x01; + } else if tag == 0x08 && generic_desktop { + // Usage + if data == 0x02 || data == 0x01 { + return true; // Mouse / Pointer + } + if data == 0x06 { + return false; // Keyboard + } + } + i += 1 + dlen; + } + false +} + unsafe fn arm_hid_transfer(rt: &mut XhciRuntime) { put_trb( rt.ep_ring, diff --git a/kernel/src/embedder/abi.rs b/kernel/src/embedder/abi.rs index ade004d..47c7201 100644 --- a/kernel/src/embedder/abi.rs +++ b/kernel/src/embedder/abi.rs @@ -197,6 +197,19 @@ pub const SYS_CLIPBOARD_GET: u64 = 0x4B4; // clipboard_get(buf_ptr, buf_len) pub const SYS_APP_CLOSE_FOREGROUND: u64 = 0x4B5; // app_close_foreground() → 0 (refocus shell, terminate fg app) pub const SYS_BEEP: u64 = 0x4B6; // beep(freq_hz, duration_ms) → 0 (PC speaker) +// App networking API — exposes the kernel TCP stack to userspace apps. +// +// The TCP socket calls (connect/write/read/close at 0x388-0x38B) are async: +// `tcp_connect` returns a fd immediately after sending SYN, so an app MUST poll +// `tcp_status` until the handshake completes before writing. (`net::tcp` doc: +// this is the correct connect-completion check — a zero-byte read probe does not +// work because an ESTABLISHED socket reports can_recv()=false until the peer +// sends.) All are CAP_NET-gated; apps are granted CAP_NET (see process::spawn). +pub const SYS_TCP_STATUS: u64 = 0x4B7; // tcp_status(fd) → 1 established / 0 connecting / -ECONNREFUSED +// Reserved for the next networking increment (DNS A-record resolution via a +// smoltcp DNS socket, reusing the proven TCP stack's ARP/routing): +pub const SYS_DNS_RESOLVE: u64 = 0x4B8; // dns_resolve(name_ptr, name_len) → ip_be u32 / negative errno + /// Mouse-cursor shapes the compositor can draw. Mirrors the subset of Flutter /// `SystemMouseCursors` kinds OSCortex maps to a distinct on-screen pointer. pub const CURSOR_SHAPE_BASIC: u32 = 0; // arrow (default) diff --git a/kernel/src/main.rs b/kernel/src/main.rs index b17516c..54b5446 100644 --- a/kernel/src/main.rs +++ b/kernel/src/main.rs @@ -326,20 +326,42 @@ pub fn kernel_main_arch_limine(map: mm::BootMemMap, hhdm_offset: u64) -> ! { /// manager, and the logger. This is the part of `kernel_main` that does not /// touch any boot protocol directly. fn shared_init_and_run() -> ! { + // On-screen boot milestones: set the phase + repaint the splash after each + // major step so a hang (e.g. on real hardware QEMU can't reproduce) leaves + // the LAST reached step on screen. No-op when the fb isn't ready. + macro_rules! bp { + ($n:expr) => { + if drivers::fb::is_ready() { + drivers::bootscreen::set_phase($n); + drivers::bootscreen::render(); + // Present immediately: once the compositor enables double- + // buffering, render() draws to the back buffer and nothing + // swaps until the engine warm-up loop — which would freeze the + // splash at "compositor" and mask a hang in any LATER phase. + // Swapping here keeps every milestone actually on screen. + drivers::fb::swap_buffers(); + } + }; + } + // ── 4. Platform drivers (input probe) ─────────────────────────────── + bp!(1); let qemu_like = arch::cpu::is_qemu_like_hypervisor(); drivers::platform::init_early(qemu_like); // ── 4b. Compositor scaffold (M13) ──────────────────────────────────── + bp!(2); compositor::init(); // ── 5. Scheduler ───────────────────────────────────────────────────── + bp!(3); sched::init(); // ── 6. Security / capabilities ──────────────────────────────────────── security::init(); // ── 7. IPC subsystem ───────────────────────────────────────────────── + bp!(4); ipc::init(); // ── 7a. Window-manager event bridge ────────────────────────────────── @@ -351,24 +373,30 @@ fn shared_init_and_run() -> ! { crate::drivers::virtio_input::init(); // ── 7b. Virtual filesystem (initramfs) ─────────────────────────────── + bp!(5); fs::init(); // ── 7c. Block, serial, and networking ───────────────────────────────── + bp!(6); drivers::platform::init_block_and_net(); // ── 7d. On-demand package delivery ──────────────────────────────── + bp!(7); pkg::init(); // ── 8. AI Cortex ───────────────────────────────────────────────────── // The Cortex boots last so every kernel subsystem is available to it. + bp!(8); cortex::init(); // ── 9. Signal APs + bring SMP online ──────────────────────────────── + bp!(9); KERNEL_INIT_DONE.store(true, Ordering::Release); #[cfg(target_arch = "x86_64")] arch::smp_init(SMP_REQUEST.response()); #[cfg(not(target_arch = "x86_64"))] arch::smp_init(None); + bp!(10); // ── 9b. Spawn init process from initramfs ──────────────────────────── match fs::lookup("/init") { diff --git a/kernel/src/mm/paging.rs b/kernel/src/mm/paging.rs index dac7443..1222818 100644 --- a/kernel/src/mm/paging.rs +++ b/kernel/src/mm/paging.rs @@ -25,72 +25,91 @@ pub(crate) static PAGE_TABLE_LOCK: Mutex<()> = Mutex::new(()); /// and hand back a guard that releases nothing until depth returns to zero. This /// is sound because all writers run on one core (the scheduler is cooperative / /// single-CPU) — there is no second core to race the page-table structure. -static PAGE_TABLE_LOCK_DEPTH: core::sync::atomic::AtomicU32 = - core::sync::atomic::AtomicU32::new(0); +/// PER-CPU re-entrancy depth for the page-table critical section. This MUST be +/// per-CPU. A single global counter was a critical SMP-safety bug: when core 0 held +/// the section (depth=1, holding the real lock) and core 1 called lock_page_table(), +/// core 1's fetch_add saw prev=1 (non-zero), concluded it was a NESTED call, and +/// BYPASSED the real lock — both cores then mutated page tables in parallel and +/// corrupted them (observed: KERNEL PANIC "corrupt PTE — a page table was +/// overwritten"). With a per-CPU counter each core's FIRST acquisition (prev==0) +/// takes the real cross-core lock and blocks until the other core releases. +static PAGE_TABLE_LOCK_DEPTH: [core::sync::atomic::AtomicU32; crate::arch::smp::MAX_CPUS] = + [const { core::sync::atomic::AtomicU32::new(0) }; crate::arch::smp::MAX_CPUS]; pub(crate) struct PageTableGuard { outer: Option>, - /// aarch64 only: DAIF captured by the OUTER acquisition before it masked - /// IRQs. Restored when the outer guard drops. Unused on nested guards (IRQs - /// stay masked for the whole outer section). + /// CPU that took this guard (index into PAGE_TABLE_LOCK_DEPTH). Stable for the + /// section: on aarch64 IRQs are masked (no preemption/migration); on x86 the + /// guard is short-lived and the outer drop runs on the same core. + cpu: usize, + /// aarch64 only: IRQ state captured by the OUTER acquisition before it masked + /// IRQs, restored on outer drop. x86 keeps its original non-masking behaviour + /// (masking the whole section there starved the app core's timer-ISR frame pump + /// during heavy demand-paging → app never presented). #[cfg(target_arch = "aarch64")] - saved_daif: u64, + saved_irq: u64, } impl Drop for PageTableGuard { fn drop(&mut self) { if self.outer.is_some() { - // Outermost guard: clear depth, release the real lock, THEN re-enable - // IRQs. The lock byte must be released before IRQs come back on, or a - // freshly-unmasked timer tick could switch to a thread that spins on - // the still-held lock with IRQs masked → single-core deadlock. - PAGE_TABLE_LOCK_DEPTH.store(0, core::sync::atomic::Ordering::Release); - let _ = self.outer.take(); // drop the MutexGuard now (IRQs still masked) + // Outermost guard: clear depth, release the real lock, THEN (aarch64) + // re-enable IRQs / (x86) re-enable preemption. The lock byte must be + // released before preemption/IRQs come back, or a freshly-allowed + // switch could land on a thread that spins on the still-held lock. + PAGE_TABLE_LOCK_DEPTH[self.cpu].store(0, core::sync::atomic::Ordering::Release); + let _ = self.outer.take(); #[cfg(target_arch = "aarch64")] - crate::arch::aarch64::cpu::interrupts_restore(self.saved_daif); + crate::arch::aarch64::cpu::interrupts_restore(self.saved_irq); } else { - PAGE_TABLE_LOCK_DEPTH.fetch_sub(1, core::sync::atomic::Ordering::Release); + PAGE_TABLE_LOCK_DEPTH[self.cpu].fetch_sub(1, core::sync::atomic::Ordering::Release); } + // x86: balance the preempt_disable() done in every lock_page_table() call + // (outer AND nested). Done last so the real lock is already released. + #[cfg(not(target_arch = "aarch64"))] + crate::process::preempt_enable(self.cpu as u32); } } -/// Acquire the page-table critical section, re-entrant on a single core. -/// -/// On aarch64 the outer acquisition also MASKS IRQs for the whole critical -/// section. The reentrant depth counter is only sound if the section cannot be -/// interleaved by another thread; but syscalls run with IRQs unmasked, so a -/// generic-timer tick can preempt a lock holder mid-section and switch to a -/// sibling, which then sees a non-zero depth and proceeds as a bogus "nested" -/// writer — desynchronising the depth counter from the real lock and eventually -/// stranding the lock held while depth reads 0 (a later outer acquire then spins -/// forever with IRQs masked in the demand-abort handler). Masking IRQs around -/// the outer section makes it genuinely uninterruptible, so nesting only ever -/// means true same-stack re-entry (e.g. a demand fault during a page-table walk, -/// which already runs IRQs-masked). x86 keeps its original behaviour. +/// Acquire the page-table critical section. Re-entrant PER-CPU, SMP-safe across +/// cores. The FIRST acquisition on each core takes the real cross-core +/// `PAGE_TABLE_LOCK`, which blocks until any OTHER core holding it releases — the +/// cross-core serialisation the old GLOBAL depth counter silently skipped (a 2nd +/// core saw depth>0, treated itself as a bogus "nested" writer, and mutated page +/// tables in parallel → corruption). aarch64 additionally masks IRQs for the whole +/// section so its per-CPU depth cannot be desynchronised by a same-core preemption; +/// x86 keeps its original non-masking behaviour (proven stable, and masking here +/// starved the app core's frame pump). #[inline] pub(crate) fn lock_page_table() -> PageTableGuard { - // Mask IRQs BEFORE touching the depth counter so the fetch_add → lock - // sequence cannot be preempted. On a nested call IRQs are already masked - // (the outer masked them); this is then an idempotent no-op. #[cfg(target_arch = "aarch64")] - let saved_daif = crate::arch::aarch64::cpu::interrupts_save_and_disable(); - let prev = PAGE_TABLE_LOCK_DEPTH.fetch_add(1, core::sync::atomic::Ordering::Acquire); + let saved_irq = crate::arch::aarch64::cpu::interrupts_save_and_disable(); + // x86: disable preemption (NOT IRQs) for the section. This pins the thread to + // its cpu (timer-preempt bails) so the per-CPU depth counter can't be stranded + // by a mid-section migration — which would re-expose the cross-core lock bypass + // — while keeping IRQs on so the app core's timer-ISR frame pump still fires. + // Returns the pinned cpu; use it for the depth slot so disable/enable balance. + #[cfg(not(target_arch = "aarch64"))] + let cpu = crate::process::preempt_disable() as usize; + #[cfg(target_arch = "aarch64")] + let cpu = crate::arch::smp::current_cpu_id() as usize; + let prev = PAGE_TABLE_LOCK_DEPTH[cpu].fetch_add(1, core::sync::atomic::Ordering::Acquire); if prev == 0 { - // First (outer) acquisition on this core: take the real lock. + // First (outer) acquisition on this core: take the real cross-core lock. let g = PAGE_TABLE_LOCK.lock(); PageTableGuard { outer: Some(g), + cpu, #[cfg(target_arch = "aarch64")] - saved_daif, + saved_irq, } } else { - // Nested acquisition: the outer guard already holds the lock (and already - // masked IRQs). `saved_daif` here is "already masked" and is discarded on - // drop — only the outer guard restores DAIF. + // Nested (same-core, same-stack) re-entry: we already hold the lock. PageTableGuard { outer: None, + cpu, #[cfg(target_arch = "aarch64")] - saved_daif, + saved_irq, } } } @@ -177,6 +196,21 @@ mod x86_64_impl { Some(phys) } + /// Checked page-table-frame → HHDM virt for READ-ONLY WALKS. Returns None if the + /// frame is null or out of addressable RAM instead of panicking. A walk can read + /// an insane table pointer when a stale pml4 reference (e.g. a futex physaddr + /// lookup during an app's crash-recovery teardown on another core) walks a page + /// table whose lower levels were freed and reused under SMP. Degrading to "not + /// mapped" keeps the kernel alive; the bare phys_to_virt (which panics on garbage, + /// a useful invariant for the WRITE paths) is unchanged. + #[inline] + fn walk_table(phys: u64) -> Option<*const u64> { + if phys == 0 || phys >= 0x0000_0004_0000_0000 { + return None; + } + Some(phys.wrapping_add(frame_allocator::hhdm_offset()) as *const u64) + } + unsafe fn ensure_next_table(entry: *mut u64) -> Option { let e = unsafe { entry.read_volatile() }; if e & PageFlags::PRESENT.bits() != 0 { @@ -321,16 +355,16 @@ mod x86_64_impl { let present = PageFlags::PRESENT.bits(); unsafe { - let pml4 = phys_to_virt(pml4_phys) as *const u64; + let pml4 = walk_table(pml4_phys)?; let pml4e = pml4.add(pml4_idx).read_volatile(); if pml4e & present == 0 { return None; } - let pdpt = phys_to_virt(pml4e & PHYS_MASK) as *const u64; + let pdpt = walk_table(pml4e & PHYS_MASK)?; let pdpte = pdpt.add(pdpt_idx).read_volatile(); if pdpte & present == 0 { return None; } - let pd = phys_to_virt(pdpte & PHYS_MASK) as *const u64; + let pd = walk_table(pdpte & PHYS_MASK)?; let pde = pd.add(pd_idx).read_volatile(); if pde & present == 0 { return None; } - let pt = phys_to_virt(pde & PHYS_MASK) as *const u64; + let pt = walk_table(pde & PHYS_MASK)?; let pte = pt.add(pt_idx).read_volatile(); if pte & present == 0 { return None; } Some(pte & PHYS_MASK) @@ -345,16 +379,16 @@ mod x86_64_impl { let present = PageFlags::PRESENT.bits(); unsafe { - let pml4 = phys_to_virt(pml4_phys) as *const u64; + let pml4 = walk_table(pml4_phys)?; let pml4e = pml4.add(pml4_idx).read_volatile(); if pml4e & present == 0 { return None; } - let pdpt = phys_to_virt(pml4e & PHYS_MASK) as *const u64; + let pdpt = walk_table(pml4e & PHYS_MASK)?; let pdpte = pdpt.add(pdpt_idx).read_volatile(); if pdpte & present == 0 { return None; } - let pd = phys_to_virt(pdpte & PHYS_MASK) as *const u64; + let pd = walk_table(pdpte & PHYS_MASK)?; let pde = pd.add(pd_idx).read_volatile(); if pde & present == 0 { return None; } - let pt = phys_to_virt(pde & PHYS_MASK) as *const u64; + let pt = walk_table(pde & PHYS_MASK)?; let pte = pt.add(pt_idx).read_volatile(); if pte & present == 0 { return None; } let phys = pte & PHYS_MASK; @@ -404,13 +438,16 @@ mod x86_64_impl { /// Walk a user PML4 and free every page frame reachable through it /// (but not the kernel-half entries 256–511, which are shared). pub fn free_user_pml4(pml4_phys: u64) { - let pml4 = phys_to_virt(pml4_phys) as *const u64; + // Use walk_table (bounds-checked) for every table-pointer deref so a garbage + // table pointer (a double-free, or an address space already disturbed by an + // SMP race) skips that subtree instead of phys_to_virt-panicking the kernel. + let pml4 = match walk_table(pml4_phys) { Some(p) => p, None => return }; // Only walk lower-half entries (0–255). for i in 0..256usize { let pml4e = unsafe { pml4.add(i).read_volatile() }; if pml4e & PageFlags::PRESENT.bits() == 0 { continue; } let pdpt_phys = pml4e & PHYS_MASK; - let pdpt = phys_to_virt(pdpt_phys) as *const u64; + let pdpt = match walk_table(pdpt_phys) { Some(p) => p, None => continue }; for j in 0..512usize { let pdpte = unsafe { pdpt.add(j).read_volatile() }; if pdpte & PageFlags::PRESENT.bits() == 0 { continue; } @@ -419,7 +456,7 @@ mod x86_64_impl { continue; } let pd_phys = pdpte & PHYS_MASK; - let pd = phys_to_virt(pd_phys) as *const u64; + let pd = match walk_table(pd_phys) { Some(p) => p, None => continue }; for k in 0..512usize { let pde = unsafe { pd.add(k).read_volatile() }; if pde & PageFlags::PRESENT.bits() == 0 { continue; } @@ -428,7 +465,7 @@ mod x86_64_impl { continue; } let pt_phys = pde & PHYS_MASK; - let pt = phys_to_virt(pt_phys) as *const u64; + let pt = match walk_table(pt_phys) { Some(p) => p, None => continue }; for l in 0..512usize { let pte = unsafe { pt.add(l).read_volatile() }; if pte & PageFlags::PRESENT.bits() != 0 { @@ -1006,7 +1043,20 @@ pub unsafe fn map_user_page_with_flags( /// Walk a user PML4 and return the physical frame already mapped at `virt`, /// or `None` if the page is not yet mapped. +/// A page-table root is only walkable if it is a plausible physical frame: non-zero +/// (phys 0 is the reserved/null frame — a freed process clears its pml4_phys to 0) +/// and within addressable RAM. Walking a freed/null root would deref phys 0 → +/// `phys_to_virt(0)` = the HHDM base, which is unmapped → unrecoverable kernel page +/// fault. This is the SMP teardown hazard: crash-recovery frees a dead app's pml4 +/// while another core still holds a stale reference (e.g. a futex physaddr lookup). +/// Treating an insane root as "nothing mapped" keeps the kernel alive. +#[inline] +fn pml4_root_walkable(pml4_phys: u64) -> bool { + pml4_phys != 0 && pml4_phys < 0x0000_0004_0000_0000 +} + pub fn translate_user_page(pml4_phys: u64, virt: u64) -> Option { + if !pml4_root_walkable(pml4_phys) { return None; } let _lock = lock_page_table(); #[cfg(target_arch = "x86_64")] { x86_64_impl::translate_user_page(pml4_phys, virt) } @@ -1019,6 +1069,7 @@ pub fn translate_user_page(pml4_phys: u64, virt: u64) -> Option { /// Walk a user PML4 and return the physical frame mapped at `virt` along with /// its writable and executable flags. pub fn translate_user_page_flags(pml4_phys: u64, virt: u64) -> Option<(u64, bool, bool)> { + if !pml4_root_walkable(pml4_phys) { return None; } let _lock = lock_page_table(); #[cfg(target_arch = "x86_64")] { x86_64_impl::translate_user_page_flags(pml4_phys, virt) } @@ -1030,6 +1081,9 @@ pub fn translate_user_page_flags(pml4_phys: u64, virt: u64) -> Option<(u64, bool /// Free all frames in a user PML4 and the PML4 itself. pub fn free_user_pml4(pml4_phys: u64) { + // Never walk/free a null or insane root: a double-free or a stale 0 would + // otherwise deref phys 0 and panic the kernel (see pml4_root_walkable). + if !pml4_root_walkable(pml4_phys) { return; } let _lock = lock_page_table(); #[cfg(target_arch = "x86_64")] x86_64_impl::free_user_pml4(pml4_phys); @@ -1070,6 +1124,7 @@ pub unsafe fn update_user_page( /// # Safety /// `pml4_phys` must be a valid allocated PML4. pub unsafe fn unmap_user_page(pml4_phys: u64, virt: u64) -> Result { + if !pml4_root_walkable(pml4_phys) { return Err("dead/null pml4"); } let _lock = lock_page_table(); #[cfg(target_arch = "x86_64")] { @@ -1092,6 +1147,7 @@ pub static UNMAP_FREED: core::sync::atomic::AtomicU64 = core::sync::atomic::Atom /// Unmap a range of virtual addresses in a specific PML4 and free their physical frames. pub fn unmap_user_range(pml4_phys: u64, start_va: u64, size: u64) { + if !pml4_root_walkable(pml4_phys) { return; } if size == 0 { return; } let start = start_va & !0xFFF; let end = (start_va + size + 4095) & !0xFFF; diff --git a/kernel/src/net/tcp.rs b/kernel/src/net/tcp.rs index d83b1b5..21c6ebf 100644 --- a/kernel/src/net/tcp.rs +++ b/kernel/src/net/tcp.rs @@ -26,9 +26,10 @@ use smoltcp::socket::tcp::{self as tcp_sock, Socket as TcpSocket}; use smoltcp::socket::udp::{self as udp_sock, Socket as UdpSocket}; use smoltcp::socket::icmp::{self as icmp_sock, Socket as IcmpSocket}; use smoltcp::socket::dhcpv4::{Socket as DhcpSocket, Event as DhcpEvent}; +use smoltcp::socket::dns::{Socket as DnsSocket, GetQueryResultError}; use smoltcp::time::Instant; use smoltcp::wire::{ - EthernetAddress, IpAddress, IpCidr, Ipv4Address, + DnsQueryType, EthernetAddress, IpAddress, IpCidr, Ipv4Address, IpEndpoint, }; @@ -139,6 +140,8 @@ struct TcpStack { icmp_handle: Option, // Phase 52: DHCP socket dhcp_handle: Option, + // DNS resolver socket (servers seeded from DHCP; default public resolver). + dns_handle: Option, ip: Ipv4Address, } @@ -203,6 +206,14 @@ pub fn init_smoltcp() { let icmp_socket = IcmpSocket::new(icmp_rx, icmp_tx); let icmp_handle = sockets.add(icmp_socket); + // DNS resolver. Seed with a public resolver so name lookups work even before + // DHCP; the DHCP ACK then swaps in the network's own servers (update_servers). + let dns_socket = DnsSocket::new( + &[IpAddress::Ipv4(Ipv4Address::new(8, 8, 8, 8))], + vec![None, None], + ); + let dns_handle = sockets.add(dns_socket); + let _g = TCP_LOCK.lock(); let stack = unsafe { &mut *TCP_STACK.0.get() }; *stack = Some(TcpStack { @@ -211,6 +222,7 @@ pub fn init_smoltcp() { handles: [None; MAX_TCP_SOCKETS], icmp_handle: Some(icmp_handle), dhcp_handle: None, + dns_handle: Some(dns_handle), ip: DEFAULT_IP, }); @@ -349,6 +361,50 @@ pub fn tcp_close(fd: usize) -> Result<(), i64> { Ok(()) } +/// Resolve a hostname to an IPv4 address (big-endian u32) via the DNS socket. +/// Drives the stack until the query completes or a ~5s timeout. Returns the +/// first A record, or a negative errno (-22 bad name, -2 failed/no A, -110 timeout). +pub fn dns_resolve(name: &str) -> Result { + let _g = TCP_LOCK.lock(); + let stack = unsafe { &mut *TCP_STACK.0.get() }; + let s = stack.as_mut().ok_or(-1i64)?; + let dns_h = s.dns_handle.ok_or(-1i64)?; + + // Start an A-record query (disjoint &mut borrows of iface + sockets). + let handle = { + let cx = s.iface.context(); + let sock = s.sockets.get_mut::(dns_h); + sock.start_query(cx, name, DnsQueryType::A).map_err(|_| -22i64)? + }; + + // Poll the stack until the resolver answers or we time out. + let start = crate::arch::rdtsc_ns(); + const TIMEOUT_NS: u64 = 5_000_000_000; + loop { + { + let mut dev = VirtioNetDev; + s.iface.poll(now(), &mut dev, &mut s.sockets); + } + let sock = s.sockets.get_mut::(dns_h); + match sock.get_query_result(handle) { + Ok(addrs) => { + for a in addrs.iter() { + if let IpAddress::Ipv4(v4) = a { + return Ok(u32::from_be_bytes(v4.octets())); + } + } + return Err(-2); // resolved but no A record + } + Err(GetQueryResultError::Pending) => {} + Err(_) => return Err(-2), // failed + } + if crate::arch::rdtsc_ns().wrapping_sub(start) > TIMEOUT_NS { + s.sockets.get_mut::(dns_h).cancel_query(handle); + return Err(-110); // ETIMEDOUT + } + } +} + /// Query current IP address (big-endian u32). pub fn current_ip_be() -> u32 { with_stack(|s| { @@ -396,6 +452,7 @@ pub fn dhcp_discover() -> u32 { if let Some(s) = stack.as_mut() { let mut dev = VirtioNetDev; s.iface.poll(now(), &mut dev, &mut s.sockets); + let mut dhcp_dns: Option> = None; if let Some(h) = s.dhcp_handle { let sock = s.sockets.get_mut::(h); if let Some(event) = sock.poll() { @@ -413,6 +470,13 @@ pub fn dhcp_discover() -> u32 { s.iface.routes_mut().add_default_ipv4_route(gw).ok(); } s.ip = ip; + if !cfg.dns_servers.is_empty() { + let mut v: Vec = Vec::new(); + for a in cfg.dns_servers.iter() { + v.push(IpAddress::Ipv4(*a)); + } + dhcp_dns = Some(v); + } } DhcpEvent::Deconfigured => { log::info!("[dhcp] deconfigured"); @@ -420,6 +484,12 @@ pub fn dhcp_discover() -> u32 { } } } + // Point the resolver at the network's own DNS servers (the DHCP + // socket borrow has ended, so s.sockets is free again). + if let (Some(dns_h), Some(servers)) = (s.dns_handle, dhcp_dns) { + s.sockets.get_mut::(dns_h).update_servers(&servers); + log::info!("[dns] {} resolver(s) from DHCP", servers.len()); + } } } if let Some(ip) = got_ip { return ip; } diff --git a/kernel/src/process/mod.rs b/kernel/src/process/mod.rs index 370443f..e1e2531 100644 --- a/kernel/src/process/mod.rs +++ b/kernel/src/process/mod.rs @@ -25,7 +25,7 @@ pub mod elf; pub mod posix_trampolines; use alloc::alloc::{alloc, dealloc, Layout}; -use core::sync::atomic::{AtomicU32, Ordering}; +use core::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use spin::Mutex; use crate::mm::{frame_allocator, paging}; @@ -175,6 +175,20 @@ pub struct Process { pub user_stack_size: u64, /// CPU core index that currently executes this process context (None if idle/blocked). pub current_cpu: Option, + /// SMP/preemption safety (Redox pattern): false while this thread is inside a + /// critical region that must not be preempted or migrated to another core + /// (e.g. holding a userspace mutex/futex). The timer-preempt path and the + /// cross-core scheduler skip a non-preemptable thread instead of switching it + /// away — preventing the mid-critical-section corruption that broke preempted + /// Dart engine threads. Defaults true. See [[redox-blueprint]]. + pub is_preemptable: bool, + /// SMP: HOME CPU core for this thread's engine (process group). The scheduler + /// runs a thread ONLY on its home core, so a whole Flutter engine stays on ONE + /// core (cooperative hand-off + per-CPU scratch never split). Different engines + /// get different home cores → parallel. 0 = BSP (default; single-core path). + /// Threads inherit their creator's home_cpu; an app host is assigned in + /// app_registry::launch. Only consulted when CPU_COUNT > 1. + pub home_cpu: u32, /// aarch64 only: full EL0 trap-frame snapshot (x0..x30, SP_EL0, ELR, SPSR, /// ESR) captured when this thread is timer-preempted at an arbitrary user /// instruction. The shared `UserRegs` carries only the x86-named subset, so @@ -271,6 +285,8 @@ impl Process { user_stack_base: 0, user_stack_size: 0, current_cpu: None, + is_preemptable: true, + home_cpu: 0, #[cfg(target_arch = "aarch64")] arch_trapframe: [0u64; 102], #[cfg(target_arch = "aarch64")] @@ -393,6 +409,24 @@ pub fn aarch64_take_fp(pid: u32) -> Option<[u64; 66]> { Some(out) } +/// aarch64 ISR-safe (try_lock) check: does `pid` have a VALID saved FP image? +/// The home-gated timer-ISR wake-assist resume uses this to NEVER re-enter a thread +/// that lacks a valid FP — resuming with no image zeroes v0–v31 incl. AAPCS64 +/// callee-saved v8–v15, corrupting an already-run engine thread (the EC=0x24 SMP +/// data abort). When the FP isn't valid the resume is skipped (the thread is left +/// Running for the cooperative path / a later tick), trading a possible extra stall +/// for never shipping corrupted FP state. Returns false on lock contention (skip). +#[cfg(target_arch = "aarch64")] +pub fn aarch64_fp_valid(pid: u32) -> bool { + match PTABLE_LOCK.try_lock() { + Some(_g) => { + let p = unsafe { &PTABLE[idx_of(pid)] }; + p.pid == pid && p.arch_fp_valid + } + None => false, + } +} + /// aarch64: read the callee-saved x24–x28 captured for `pid` (used to populate /// EnterUserRegs so the cooperative/re-exec resume restores them). #[cfg(target_arch = "aarch64")] @@ -541,6 +575,7 @@ impl PTableLock { PTABLE_LOCK_RECURSION[my_cpu as usize] = 1; PTABLE_LOCK_GUARD = Some(core::mem::transmute(g)); } + preempt_disable_cpu(my_cpu); // non-preemptable while holding PTABLE_LOCK PTableGuard { is_outer: true } } } @@ -559,6 +594,7 @@ impl PTableLock { PTABLE_LOCK_RECURSION[my_cpu as usize] = 1; PTABLE_LOCK_GUARD = Some(core::mem::transmute(g)); } + preempt_disable_cpu(my_cpu); // non-preemptable while holding PTABLE_LOCK Some(PTableGuard { is_outer: true }) } else { None @@ -576,6 +612,7 @@ impl Drop for PTableGuard { PTABLE_LOCK.holder.store(0xFFFF_FFFF, Ordering::Release); PTABLE_LOCK_GUARD = None; } + preempt_enable_cpu(my_cpu); // preemptable again once PTABLE_LOCK is released } else { let my_cpu = ptable_cpu_idx(); unsafe { @@ -592,6 +629,85 @@ static mut PTABLE_LOCK_RECURSION: [u32; 64] = [0; 64]; pub(crate) static PTABLE_LOCK: PTableLock = PTableLock::new(); +// ── SMP M1: per-CPU preempt-disable + non-preemptable bail ─────────────────── +// +// Redox-blueprint pattern #2 ("the key to safe preemption"): never switch AWAY +// from a CPU that is in a critical section. Here the critical section is "this +// CPU holds PTABLE_LOCK" — auto-tracked by incrementing this CPU's preempt depth +// on the OUTER PTABLE_LOCK acquire and decrementing it on the outer release. The +// timer-preempt path checks `preempt_disabled()` and bails when set. +// +// Why this matters even single-core (x86): the timer ISR's `timer_preempt_switch_ +// try` does a *recursive* PTABLE_LOCK `try_lock`, which SUCCEEDS when the +// interrupted thread already holds the lock — it could then switch threads while +// the lock is logically held, leaving `holder` pinned to this CPU and wedging +// every later PTABLE_LOCK user. Bailing when preempt-disabled closes that hole. +// One slot per CPU; only the owning CPU touches its slot (+ its own ISR reads it). +pub static PREEMPT_DEPTH: [AtomicU32; crate::arch::smp::MAX_CPUS] = + [const { AtomicU32::new(0) }; crate::arch::smp::MAX_CPUS]; + +#[inline(always)] +fn preempt_disable_cpu(cpu: u32) { + if (cpu as usize) < crate::arch::smp::MAX_CPUS { + PREEMPT_DEPTH[cpu as usize].fetch_add(1, Ordering::AcqRel); + } +} +#[inline(always)] +fn preempt_enable_cpu(cpu: u32) { + if (cpu as usize) < crate::arch::smp::MAX_CPUS { + // Saturating: never wrap below zero on an unbalanced release. + let slot = &PREEMPT_DEPTH[cpu as usize]; + let mut cur = slot.load(Ordering::Acquire); + while cur > 0 { + match slot.compare_exchange_weak(cur, cur - 1, Ordering::AcqRel, Ordering::Acquire) { + Ok(_) => break, + Err(v) => cur = v, + } + } + } +} + +/// True if THIS CPU is currently non-preemptable (in a PTABLE_LOCK critical +/// section, or another future preempt-disabled region). The timer-preempt path +/// must not switch away when this is set. +#[inline] +pub fn preempt_disabled() -> bool { + let c = ptable_cpu_idx() as usize; + c < crate::arch::smp::MAX_CPUS && PREEMPT_DEPTH[c].load(Ordering::Acquire) > 0 +} + +/// Disable preemption on the CURRENT cpu and return that cpu index. The timer- +/// preempt path bails while PREEMPT_DEPTH>0, so the caller's thread cannot be +/// switched away or migrated — WITHOUT masking IRQs (timer ticks still fire, so a +/// frame pump on this core keeps running). Pair with `preempt_enable(cpu)` using +/// the RETURNED cpu so the dec lands on the same slot even if a migration somehow +/// occurred in the tiny pre-disable window. Used by the page-table critical section +/// to keep its per-CPU re-entrancy depth sound on x86 (no IRQ mask there). +#[inline] +pub fn preempt_disable() -> u32 { + // Briefly mask IRQs so reading the current cpu and bumping its PREEMPT_DEPTH is + // ATOMIC — otherwise a timer preemption between the two could migrate the thread + // and we'd disable preemption on the WRONG cpu (the thread then runs preemptible + // on its new cpu, and a page-table section's per-CPU depth gets stranded on the + // old cpu → cross-core lock bypass → page-table corruption, observed ~1/6). Once + // PREEMPT_DEPTH is bumped, preemption is disabled, so after we restore IRQs the + // thread can no longer migrate; the cpu stays valid for the whole section. + let irq = crate::arch::interrupts_save_and_disable(); + let cpu = ptable_cpu_idx(); + preempt_disable_cpu(cpu); + crate::arch::interrupts_restore(irq); + cpu +} +#[inline] +pub fn preempt_enable(cpu: u32) { + preempt_enable_cpu(cpu); +} + +/// Global context-switch serialization lock (Redox-blueprint #2). Reserved for the +/// M4 two-layer `switch_to`; declared now so the foundation is in one place. +#[allow(dead_code)] +pub static CONTEXT_SWITCH_LOCK: AtomicBool = AtomicBool::new(false); + /// Bitmask of processes with a pending wake-up to be processed lock-free. pub static PENDING_WAKES: [AtomicU32; 8] = [ AtomicU32::new(0), @@ -795,8 +911,30 @@ pub fn spawn_with_bootstrap( p.caps = if bootstrap.rdi == crate::app_registry::HOST_MODE_SHELL { crate::security::ALL_CAPS } else { - crate::security::NO_CAPS + // Apps get NET so they can use the userspace TCP/DNS socket API + // (the abstraction layer). Everything else stays gated. The security + // phase will replace this blanket grant with a per-app declared/ + // prompted NET permission rather than granting every app by default. + crate::security::Capabilities::NET + }; + // SMP home-core assignment — done HERE, atomically before the process + // becomes Running, so there is no window where core 0 could schedule a + // freshly-spawned app host (home_cpu still defaulting to 0) and leave a + // stale `current_cpu` claim that the real home core then can't reclaim. + // An app host (HOST_MODE_APP) is pinned to a dedicated application core; + // the shell and everything else stays on the BSP (0). On single-core + // boots pick_home_cpu_for_app returns 0, so this is a no-op there. + p.home_cpu = if bootstrap.rdi == crate::app_registry::HOST_MODE_APP { + pick_home_cpu_for_app() + } else { + 0 }; + // Wake the dormant application core: it idles (wfi) until it has home-pinned + // work. Flag it now (atomic, no lock); the reschedule IPI at the end of this + // fn then kicks it out of wfi to engage scheduling. + if p.home_cpu != 0 { + mark_cpu_has_work(p.home_cpu); + } p.syscall_stack_base = sys_stack_base; p.syscall_stack_top = sys_stack_top; p.xstate = XStateBuf::default(); @@ -827,6 +965,13 @@ pub fn spawn_with_bootstrap( name, pid, entry, bootstrap.rdi, bootstrap.rsi, bootstrap.rdx, bootstrap.parent_pid, caps_of(pid) ); + // SMP: the new process is born Running and may be HOME-PINNED to another core + // (an app host → an AP). The spawning core (the shell, on the BSP) must poke the + // other cores so the home core wakes from its idle `hlt` and picks the newborn + // up immediately, instead of waiting for its next timer tick — which a busy/ + // starved sibling core (notably under TCG SMP) may delay indefinitely, leaving + // the freshly-launched app stuck unscheduled (the "app never ran" freeze). + crate::arch::smp::broadcast_resched_ipi(); Ok(pid) } @@ -982,6 +1127,83 @@ pub fn kill(pid: u32) -> Result<(), &'static str> { Ok(()) } +/// Mark a thread (non-)preemptable (Redox pattern). Set false while it holds a +/// userspace lock so the timer-preempt path + cross-core scheduler won't switch +/// it away / migrate it mid-critical-section. ISR/lock-free-safe via try_lock. +pub fn set_preemptable(pid: u32, value: bool) { + if let Some(_g) = PTABLE_LOCK.try_lock() { + let p = unsafe { &mut PTABLE[idx_of(pid)] }; + if p.pid == pid { + p.is_preemptable = value; + } + } +} + +/// SMP: pin `pid` (and its engine, since threads inherit it) to a home CPU core. +/// The SMP home core a process (and its thread group) is pinned to. 0 = BSP. +pub fn home_cpu_of(pid: u32) -> u32 { + let _g = PTABLE_LOCK.lock(); + let p = unsafe { &PTABLE[idx_of(pid)] }; + if p.pid == pid { p.home_cpu } else { 0 } +} + +/// Per-application-core "has home-pinned work" flag. A secondary core (AP) stays +/// fully dormant — `wfi`, no run-queue polling, its timer ISR does nothing — until +/// an app is actually pinned to it. Apps only launch well after boot, so before the +/// first launch an AP has nothing to run, and engaging it early makes its timer-ISR +/// work contend with the BSP's timing-sensitive shell first-frame bring-up (observed +/// on aarch64-HVF: engaging at flutter_init_ready stalled the shell at present=1). +/// Set (once) when an app host is assigned to a core; the spawn's reschedule IPI then +/// kicks that core out of `wfi` to engage. Never cleared (after the first launch the +/// AP schedules normally — boot is long over). +pub static CPU_HAS_HOME_WORK: [core::sync::atomic::AtomicBool; crate::arch::smp::MAX_CPUS] = + [const { core::sync::atomic::AtomicBool::new(false) }; crate::arch::smp::MAX_CPUS]; + +#[inline] +pub fn mark_cpu_has_work(cpu: u32) { + if (cpu as usize) < crate::arch::smp::MAX_CPUS { + CPU_HAS_HOME_WORK[cpu as usize].store(true, Ordering::Release); + } +} +#[inline] +pub fn cpu_has_home_work(cpu: u32) -> bool { + (cpu as usize) < crate::arch::smp::MAX_CPUS + && CPU_HAS_HOME_WORK[cpu as usize].load(Ordering::Acquire) +} + +/// ISR-safe (try-lock) read of a process's home core. Returns None if the table +/// lock is momentarily contended (the caller simply skips this tick). Used by the +/// per-core timer-ISR wake-assist so a core only ever touches its own home threads. +pub fn home_cpu_of_try(pid: u32) -> Option { + let _g = PTABLE_LOCK.try_lock()?; + let p = unsafe { &PTABLE[idx_of(pid)] }; + Some(if p.pid == pid { p.home_cpu } else { 0 }) +} + +/// Round-robin a freshly-launched engine onto a core OTHER than the BSP (0, the +/// shell's core) when >1 core is online; else the BSP. First app → core 1. +pub fn pick_home_cpu_for_app() -> u32 { + let n = crate::arch::smp::CPU_COUNT.load(Ordering::Acquire); + if n <= 1 { + return 0; + } + static NEXT_APP_CPU: AtomicU32 = AtomicU32::new(0); + let k = NEXT_APP_CPU.fetch_add(1, Ordering::Relaxed); + 1 + (k % (n - 1)) +} + +/// Is `pid` currently preemptable? Defaults true for unknown/dead pids so callers +/// never wedge on a stale id. +pub fn is_preemptable(pid: u32) -> bool { + if let Some(_g) = PTABLE_LOCK.try_lock() { + let p = unsafe { &PTABLE[idx_of(pid)] }; + if p.pid == pid { + return p.is_preemptable; + } + } + true +} + /// Set currently active userspace PID (called by exec/scheduler glue). pub fn set_current_pid(pid: u32) { crate::arch::smp::this_cpu().current_pid.store(pid, Ordering::Release); @@ -1131,6 +1353,15 @@ pub fn try_claim_cpu_for_try(pid: u32, my_cpu: u32) -> bool { let idx = idx_of(pid); let p = unsafe { &mut PTABLE[idx] }; if p.pid == pid && p.state == ProcState::Running { + // SMP home-core: only the target's HOME core may claim it. This is what + // lets the per-core timer-ISR wake-assist run on every core safely — the + // BSP wakes only home_cpu==0 threads (the shell), an AP wakes only its + // own home-pinned threads (a launched app). Two cores never claim the + // same user thread. On single-core home_cpu is always 0 == my_cpu. + let smp = crate::arch::smp::CPU_COUNT.load(Ordering::Acquire) > 1; + if smp && p.home_cpu != my_cpu { + return false; + } if p.current_cpu.is_none() || p.current_cpu == Some(my_cpu) { p.current_cpu = Some(my_cpu); return true; @@ -1272,6 +1503,17 @@ pub fn next_runnable_pid_locked(current: u32, my_cpu: u32) -> Option { } } + // SMP: with >1 core online, an app host is pinned to its own APPLICATION core + // (home_cpu, set in spawn_with_bootstrap), so the launched engine runs with its + // OWN per-CPU GPR scratch — never sharing the cooperative context that corrupts + // a 2nd engine on a single core. We KEEP foreground-exclusive (the shell is + // suspended while an app is foreground): letting the shell run concurrently with + // the app trips Dart's isolate-confinement check ("Isolate main is owned by os + // thread X, failed to schedule from os thread Y" → sys_abort). Exclusive + the + // `home_cpu == my_cpu` filter together give: the app runs ALONE on its core, the + // shell stays parked on the BSP — separate scratch, no concurrent shell engine. + let smp = crate::arch::smp::CPU_COUNT.load(Ordering::Acquire) > 1; + let focus = fg; let input_target = if focus != 0 { focus } else { 1 }; // Only honour the input shortcut when the target belongs to the foreground @@ -1280,7 +1522,9 @@ pub fn next_runnable_pid_locked(current: u32, my_cpu: u32) -> Option { if input_ok && input_target != 0 && crate::wm::input_pending_for(input_target) > 0 { if current != input_target { let target = unsafe { &mut PTABLE[idx_of(input_target)] }; - if target.pid == input_target && target.state == ProcState::Running { + if target.pid == input_target && target.state == ProcState::Running + && (!smp || target.home_cpu == my_cpu) + { if target.current_cpu.is_none() || target.current_cpu == Some(my_cpu) { target.current_cpu = Some(my_cpu); return Some(input_target); @@ -1294,7 +1538,9 @@ pub fn next_runnable_pid_locked(current: u32, my_cpu: u32) -> Option { // the SHELL's own baton triggers this (the app's baton runs the app instead). if !exclusive && current != 1 && crate::wm::embedder_baton_due(1) { let embedder = unsafe { &mut PTABLE[idx_of(1)] }; - if embedder.pid == 1 && embedder.state == ProcState::Running { + if embedder.pid == 1 && embedder.state == ProcState::Running + && (!smp || embedder.home_cpu == my_cpu) + { if embedder.current_cpu.is_none() || embedder.current_cpu == Some(my_cpu) { embedder.current_cpu = Some(my_cpu); return Some(1); @@ -1312,6 +1558,9 @@ pub fn next_runnable_pid_locked(current: u32, my_cpu: u32) -> Option { let idx = (start + off) % MAX_PROCS; let p = unsafe { &mut PTABLE[idx] }; if p.pid != 0 && p.state == ProcState::Running { + if smp && p.home_cpu != my_cpu { + continue; // SMP: this thread's engine is pinned to another core + } if exclusive && get_group_leader_locked(p.pid) != fg_group { continue; // not in the foreground app's group — skip while it runs } @@ -1328,6 +1577,7 @@ pub fn next_runnable_pid_locked(current: u32, my_cpu: u32) -> Option { if res.is_none() && current != 0 { let c = unsafe { &mut PTABLE[idx_of(current)] }; if c.pid == current && c.state == ProcState::Running + && (!smp || c.home_cpu == my_cpu) && (!exclusive || get_group_leader_locked(current) == fg_group) { if c.current_cpu.is_none() || c.current_cpu == Some(my_cpu) { @@ -1372,6 +1622,20 @@ pub fn get_user_context(pid: u32) -> Option { }) } +/// Page-table root (PML4 on x86, TTBR0 on aarch64) of any LIVE process — including +/// Blocked ones (unlike `get_user_context`, which requires Running). Used by the +/// futex to compute a waiter's PHYSICAL futex address for SMP-correct wake scoping +/// (a Blocked futex waiter must still be locatable). Returns None for Dead slots. +pub fn pml4_phys_of(pid: u32) -> Option { + let _g = PTABLE_LOCK.lock(); + let p = unsafe { &PTABLE[idx_of(pid)] }; + if p.pid == pid && p.state != ProcState::Dead { + Some(p.pml4_phys) + } else { + None + } +} + pub fn current_syscall_stack_top() -> Option { let pid = current_pid(); if pid == 0 { @@ -1427,14 +1691,14 @@ pub fn spawn_thread( arg: u64, stack_size: usize, ) -> Result { - let (parent_pml4_phys, owning_pid) = { + let (parent_pml4_phys, owning_pid, parent_home_cpu) = { let _g = PTABLE_LOCK.lock(); let p = unsafe { &PTABLE[idx_of(parent_pid)] }; if p.pid != parent_pid || p.state == ProcState::Dead { log::error!("[spawn_thread] parent not found pid={}", parent_pid); return Err("parent not found"); } - (p.pml4_phys, get_group_leader_locked(parent_pid)) + (p.pml4_phys, get_group_leader_locked(parent_pid), p.home_cpu) }; let tid = match alloc_pid() { @@ -1576,6 +1840,7 @@ pub fn spawn_thread( let p = unsafe { &mut PTABLE[idx_of(tid)] }; p.pid = tid; p.pml4_phys = parent_pml4_phys; // shared with parent + p.home_cpu = parent_home_cpu; // SMP: thread stays on its engine's core p.regs = regs; p.state = ProcState::Running; p.exit_code = 0; @@ -1707,30 +1972,64 @@ pub fn mark_init_launched() { PENDING_INIT_PID.store(0, Ordering::Release); } -pub fn enter_user_by_pid_noreturn(pid: u32) -> ! { +pub fn enter_user_by_pid_noreturn(req_pid: u32) -> ! { let my_cpu = crate::arch::smp::this_cpu().cpu_id; let old_pid = current_pid(); + // ── SMP claim resolution ──────────────────────────────────────────────── + // Release our previous claim, then claim a pid THIS core may run. If the + // requested pid is owned by ANOTHER core (a cooperative hand-off targeted a + // thread the other core is already running), re-pick a runnable thread for + // this core instead of panicking — this is the SMP-correct back-off that + // makes every hand-off call site safe without per-site auditing. On a single + // core this always resolves to `req_pid` (claim is free), so behaviour there + // is unchanged. + { + let _g = PTABLE_LOCK.lock(); + // STICKY CPU AFFINITY: do NOT release `old_pid`'s claim here. Releasing it on + // every hand-off makes every yielded thread instantly pickable by the other + // core, so both cores chase the same hot threads and livelock (rendering + // stalls at ~1 frame under smp=2). Keeping the claim sticky pins each thread + // to the core that first ran it; unclaimed threads distribute across cores as + // they become runnable, and a blocked thread is reclaimed by its own core. + // (current_cpu is reset to None when the PCB is freed/reused on exit.) + if my_cpu == 0 { + crate::arch::smp::set_sched_ready(); + } + } + let pid = loop { + let got = { + let _g = PTABLE_LOCK.lock(); + let p = unsafe { &mut PTABLE[idx_of(req_pid)] }; + if p.pid == req_pid + && (p.current_cpu.is_none() || p.current_cpu == Some(my_cpu)) + { + p.current_cpu = Some(my_cpu); + true + } else { + false + } + }; + if got { + break req_pid; + } + // req_pid is owned by another core — pick a different runnable thread for + // this core (next_runnable_pid claims it for my_cpu). If none, idle + retry. + if let Some(other) = next_runnable_pid(current_pid()) { + if other != req_pid { + break other; + } + } + unsafe { crate::arch::enable_and_halt() }; + }; + let (pml4_phys, rip, rsp, rflags, rax, rdi, rsi, rdx, rbx, rbp, r8, r9, r10, r11, r12, r13, r14, r15, rcx, syscall_stack_top, fs_base, errno_to_deliver, preempted_by_timer) = { let _g = PTABLE_LOCK.lock(); - if old_pid != 0 && old_pid != pid { - let old_p = unsafe { &mut PTABLE[idx_of(old_pid)] }; - if old_p.pid == old_pid && old_p.current_cpu == Some(my_cpu) { - old_p.current_cpu = None; - } - } let p = unsafe { &mut PTABLE[idx_of(pid)] }; assert_eq!(p.pid, pid, "[Process] enter_user_by_pid_noreturn: stale PID"); - if let Some(other_cpu) = p.current_cpu { - if other_cpu != my_cpu { - panic!( - "[Process] enter_user_by_pid_noreturn: PID {} is already running on CPU {}, but CPU {} is trying to enter it!", - pid, other_cpu, my_cpu - ); - } - } + // Claim already established above; ensure it's ours (re-pick set it too). p.current_cpu = Some(my_cpu); let e = p.errno_to_deliver; p.errno_to_deliver = 0; // consume it @@ -2057,6 +2356,11 @@ fn user_launch_task() { /// /// Returns `None` if there is no other runnable thread (no switch occurs). pub fn timer_preempt_switch(cur_pid: u32, cur_regs: &UserRegs) -> Option<(u32, UserRegs, u64)> { + // SMP M1 (non-preemptable bail): never switch away while this CPU holds + // PTABLE_LOCK — doing so would leave the lock pinned to this CPU and wedge it. + if preempt_disabled() { + return None; + } let my_cpu = crate::arch::smp::current_cpu_id(); let fs = crate::arch::cpu::get_fs_base(); @@ -2122,6 +2426,12 @@ pub fn timer_preempt_switch(cur_pid: u32, cur_regs: &UserRegs) -> Option<(u32, U /// Try-lock variant of `timer_preempt_switch` to avoid ISR deadlocks. pub fn timer_preempt_switch_try(cur_pid: u32, cur_regs: &UserRegs) -> Option<(u32, UserRegs, u64)> { + // SMP M1 (non-preemptable bail): if the interrupted thread holds PTABLE_LOCK, + // a recursive try_lock below would SUCCEED and let us switch threads mid- + // critical-section, pinning the lock to this CPU forever. Bail instead. + if preempt_disabled() { + return None; + } let my_cpu = crate::arch::smp::current_cpu_id(); let fs = crate::arch::cpu::get_fs_base(); diff --git a/kernel/src/sched/mod.rs b/kernel/src/sched/mod.rs index ccfb8c7..dad5d81 100644 --- a/kernel/src/sched/mod.rs +++ b/kernel/src/sched/mod.rs @@ -81,14 +81,44 @@ pub fn init() { MAX_TASKS, STACK_SIZE / 1024, SCHED_SLICE); } -/// AP parking stub. +/// AP scheduler entry — each application core runs HOME-PINNED user threads. /// -/// The current scheduler is a single global queue/state machine and is not yet -/// safe for concurrent scheduling decisions from multiple CPUs. Park APs with -/// interrupts disabled so only the BSP drives preemption. +/// SMP design: an engine (process group) is pinned to a `home_cpu` (see +/// `process::pick_home_cpu_for_app`). `next_runnable_pid` only returns a thread +/// whose `home_cpu == my_cpu`, so the BSP and each AP run disjoint engines on +/// disjoint per-CPU GPR scratch — no shared cooperative context to corrupt. +/// The AP loops: pick a home-pinned runnable thread, enter it; if none, halt +/// until the next interrupt (timer tick / reschedule IPI) re-checks the queue. pub fn ap_start(cpu_idx: u32) -> ! { - log::info!("[Sched] AP cpu={} ready, parked (BSP-only scheduler)", cpu_idx); - crate::arch::halt_forever(); + // Wait until the BSP has claimed the init thread and opened SMP scheduling, + // so we never race the BSP to claim pid 1 during early bring-up. + while !crate::arch::smp::sched_ready() { + core::hint::spin_loop(); + } + // Stay fully PARKED (idle hlt, no run-queue polling) until the shell's engine + // is up. Apps only launch after that, so the AP has no home-pinned work before + // then — and polling next_runnable_pid (PTABLE_LOCK) every timer tick contends + // with the BSP during the timing-sensitive shell first-frame bring-up, which + // under TCG SMP can stall the shell at boot (present=0). An IPI/timer wakes the + // hlt; we just re-check the gate until the shell is ready, then join scheduling. + while !crate::wm::flutter_init_ready() { + crate::arch::enable_and_halt(); + } + log::error!("[Sched] AP cpu={} entering home-pinned scheduler loop", cpu_idx); + + loop { + // current=0: this core has no resident process between entries. The + // home_cpu filter inside next_runnable_pid (keyed off this_cpu().cpu_id) + // restricts the pick to engines pinned to THIS core. + if let Some(pid) = crate::process::next_runnable_pid(0) { + // enter_user_by_pid_noreturn never returns to the caller; on the next + // syscall/preemption the AP re-enters this loop via its idle path. + crate::process::enter_user_by_pid_noreturn(pid); + } + // Nothing home-pinned to this core right now — wait for an interrupt + // (timer tick or reschedule IPI) then re-scan. + crate::arch::enable_and_halt(); + } } /// Register the **current** execution context (BSP idle loop) as the idle task. diff --git a/kernel/src/syscall/dispatch.rs b/kernel/src/syscall/dispatch.rs index 8d1e7a7..978f994 100644 --- a/kernel/src/syscall/dispatch.rs +++ b/kernel/src/syscall/dispatch.rs @@ -50,16 +50,6 @@ pub extern "C" fn dispatch_fast(number: u64, arg0: u64, arg1: u64, arg2: u64, ar let user_rip = crate::arch::syscall::user_rip(); record_syscall_trace(number, arg0, arg1, arg2, user_rip); - // Trace the first 32 syscalls made by pid=7 so we can see exactly which - // call triggers "Poll failed:" and from where. - { - static PID7_TRACE: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0); - if crate::process::current_pid() == 7 { - let n = PID7_TRACE.fetch_add(1, core::sync::atomic::Ordering::Relaxed); - let _ = n; - } - } - // Post-pid2-exit trace window: log every syscall (any pid) for the first // POSTEXIT_TRACE_LIMIT calls after PID-2's exit, so we can see exactly // what pid=1 does once it resumes from pthread_cond_wait. @@ -501,6 +491,8 @@ pub extern "C" fn dispatch_fast(number: u64, arg0: u64, arg1: u64, arg2: u64, ar 0x38A => sys_tcp_read(arg0, arg1, arg2), 0x38B => sys_tcp_close(arg0), 0x38C => sys_dhcp_discover(), + eabi::SYS_TCP_STATUS => sys_tcp_status(arg0), + eabi::SYS_DNS_RESOLVE => sys_dns_resolve(arg0, arg1), // Phase 51 — ext2 read-only filesystem 0x38D => sys_ext2_mount(), @@ -822,6 +814,7 @@ fn required_cap(number: u64) -> Option { || n == eabi::SYS_NET_SEND || n == eabi::SYS_NET_RECV => Some(Capabilities::NET), 0x388 | 0x389 | 0x38A | 0x38B | 0x38C => Some(Capabilities::NET), + n if n == eabi::SYS_TCP_STATUS || n == eabi::SYS_DNS_RESOLVE => Some(Capabilities::NET), _ => None, } } diff --git a/kernel/src/syscall/handlers/engine.rs b/kernel/src/syscall/handlers/engine.rs index 4b23d26..7be0109 100644 --- a/kernel/src/syscall/handlers/engine.rs +++ b/kernel/src/syscall/handlers/engine.rs @@ -1325,6 +1325,36 @@ pub(crate) fn sys_tcp_close(fd: u64) -> i64 { } } +/// Poll an outbound TCP connection's state. arg0 = fd. +/// Returns 1 once the handshake has completed (the socket may send), 0 while +/// still connecting, or a negative errno (e.g. -111 ECONNREFUSED) if the +/// connection was refused/reset. This is the correct way to wait for connect: +/// a zero-byte read probe can't tell "connected, peer silent" from "connecting". +pub(crate) fn sys_tcp_status(fd: u64) -> i64 { + match crate::net::tcp::tcp_is_established(fd as usize) { + Ok(true) => 1, + Ok(false) => 0, + Err(e) => e, + } +} + +/// Resolve a hostname to an IPv4 address. arg0=name_ptr, arg1=name_len. +/// Returns the address as a big-endian u32 (≥0), or a negative errno. +pub(crate) fn sys_dns_resolve(name_ptr: u64, name_len: u64) -> i64 { + let bytes = match unsafe { read_user_bytes(name_ptr, (name_len as usize).min(255)) } { + Some(b) => b, + None => return -14, // EFAULT + }; + let name = match core::str::from_utf8(bytes) { + Ok(s) => s, + Err(_) => return -22, // EINVAL + }; + match crate::net::tcp::dns_resolve(name) { + Ok(ip) => ip as i64, + Err(e) => e, + } +} + /// Trigger DHCP DISCOVER. Returns assigned IP as BE u32, or 0 on failure. pub(crate) fn sys_dhcp_discover() -> i64 { crate::net::tcp::dhcp_discover() as i64 diff --git a/kernel/src/syscall/handlers/futex.rs b/kernel/src/syscall/handlers/futex.rs index fb86893..f2be028 100644 --- a/kernel/src/syscall/handlers/futex.rs +++ b/kernel/src/syscall/handlers/futex.rs @@ -324,7 +324,36 @@ fn futex_pending_take(addr: u64) -> bool { false } +/// SMP M2: the PHYSICAL address backing a process's futex word at virtual `addr`. +/// Two contexts share a futex iff this matches — the correct, address-space- +/// independent identity (Redox blueprint #4). Returns None if the page isn't +/// mapped in that process (caller then falls back to group-leader scoping). +fn futex_phys_of(pid: u32, addr: u64) -> Option { + let p4 = crate::process::pml4_phys_of(pid)?; + let frame = crate::mm::paging::translate_user_page(p4, addr & !0xFFF)?; + Some(frame | (addr & 0xFFF)) +} + pub(crate) fn futex_wake_waiters(addr: u64, count: u32) -> i64 { + // CRITICAL: FUTEX_WAITERS is keyed by the userspace futex/mutex ADDRESS only, + // but that address is process-local. The shell (pid 1) and any launched app + // (e.g. pid 10) are distinct address spaces that load the same libc / Flutter + // engine .so at IDENTICAL virtual addresses, so their internal pthread + // mutex/cond addresses COLLIDE in this table. Waking the first `count` waiters + // blindly can wake — and remove — the WRONG process's waiter, leaving the + // intended one parked forever (the post-app-launch render freeze: a render + // thread stuck in pthread_mutex_lock while the unlock woke a sibling-process + // waiter instead). Only wake waiters that share the caller's address space. + // (Kernel/ISR context, pid 0, has no group and wakes everyone — preserves the + // force-wake bring-up path.) + let caller = crate::process::current_pid(); + let caller_grp = if caller == 0 { 0 } else { crate::process::get_group_leader(caller) }; + // SMP M2: prefer PHYSICAL-address identity over group-leader scoping. The + // caller's physical futex address is the authoritative key — two contexts + // share the futex iff they map `addr` to the same physical page (correct even + // across cores, and across the shell/app shared-VA collision). Kernel/ISR + // context (caller==0) still wakes everyone (the bring-up force-wake path). + let caller_phys = if caller == 0 { None } else { futex_phys_of(caller, addr) }; let wake_list = { let mut table = FUTEX_WAITERS.lock(); let Some(waiters) = table.get_mut(&addr) else { @@ -337,10 +366,26 @@ pub(crate) fn futex_wake_waiters(addr: u64, count: u32) -> i64 { } return 0; }; - let wake_count = waiters.len().min(count as usize); - let mut woke = Vec::with_capacity(wake_count); - for _ in 0..wake_count { - woke.push(waiters.remove(0)); + let mut woke = Vec::with_capacity((count as usize).min(waiters.len())); + let mut i = 0; + while i < waiters.len() && woke.len() < count as usize { + let w = waiters[i]; + // Physical-address identity is authoritative when both resolve; fall + // back to group-leader scoping only if a translation fails (so we + // never DROP a wake the old path would have allowed). + let same_space = if caller_grp == 0 { + true + } else if let (Some(cp), Some(wp)) = (caller_phys, futex_phys_of(w, addr)) { + cp == wp + } else { + crate::process::get_group_leader(w) == caller_grp + }; + if same_space { + woke.push(w); + waiters.remove(i); + } else { + i += 1; + } } if waiters.is_empty() { table.remove(&addr); diff --git a/kernel/src/syscall/poll.rs b/kernel/src/syscall/poll.rs index 1c82b90..271b554 100644 --- a/kernel/src/syscall/poll.rs +++ b/kernel/src/syscall/poll.rs @@ -402,7 +402,17 @@ pub fn force_wake_all_task_runners(reason: &str) -> (usize, usize) { let mut tbl = TIMERFD_TABLE.lock(); for fd in &tfds { if let Some(t) = tbl.get_mut(fd) { - t.pending = t.pending.saturating_add(1); + // Mark ready WITHOUT accumulating. This is a spurious wake to break + // an epoll_wait parking deadlock, NOT a real expiration. The old + // `saturating_add(1)` inflated an actively-serviced timerfd's pending + // every time force-wake fired (frequent during app bring-up), so the + // next timerfd read returned a large BOGUS expiration count — a + // prime suspect for corrupting dart:io's timeout processing (the + // intermittent app-launch EventHandler #GP). Cap at 1: still reports + // EPOLLIN to unpark the waiter, but never piles up fake expirations. + if t.pending == 0 { + t.pending = 1; + } all_tfds.push(*fd); } } diff --git a/kernel/src/syscall/posix.rs b/kernel/src/syscall/posix.rs index 7da855c..0bcb147 100644 --- a/kernel/src/syscall/posix.rs +++ b/kernel/src/syscall/posix.rs @@ -914,11 +914,19 @@ pub fn sys_pthread_mutex_lock(mutex: u64, sys_nr: u64) -> i64 { log::warn!("[startup-mutex] after-waiter-add pid={} mutex={:#x}", pid, mutex); } - if pid >= 2 && crate::process::get_group_leader(pid) == 1 { + // Deadlock breaker for ANY embedder host group (not just the shell, pid 1). + // A render thread can contend on a mutex held by a sibling parked in an + // infinite epoll_wait on a disarmed timerfd; force-waking the task runners + // pokes that timerfd so the holder runs and releases the mutex. This used + // to be gated on `group_leader == 1`, so it only ever rescued the SHELL — + // every launched APP (group leader = its own host pid, e.g. 10) was skipped + // and wedged a few frames after launch. Generalize to the thread's own host. + let host = crate::process::get_group_leader(pid); + if pid >= 2 && host != pid && host != 0 { static ENGINE_MUTEX_WAIT_KICK: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0); let kick = ENGINE_MUTEX_WAIT_KICK.fetch_add(1, Ordering::Relaxed); - crate::process::set_state(1, crate::process::ProcState::Running); + crate::process::set_state(host, crate::process::ProcState::Running); if kick < 32 || (kick & 15) == 0 { if trace_startup_mutex { log::warn!("[startup-mutex] before-force-wake pid={} mutex={:#x}", pid, mutex); @@ -964,7 +972,10 @@ pub fn sys_pthread_mutex_lock(mutex: u64, sys_nr: u64) -> i64 { let mut mutex_handoff_target = || { crate::process::next_runnable_pid(pid).or_else(|| { - if pid >= 2 && crate::process::get_group_leader(pid) == 1 { + // Generalized from `group_leader == 1` (shell only) to any embedder + // worker thread, so a launched app's render threads also hand off to + // their host / siblings instead of stalling. + if pid >= 2 && crate::process::get_group_leader(pid) != pid { super::prefer_embedder_if_baton_due(pid) .or_else(|| crate::process::next_runnable_sibling_thread(pid)) .or_else(|| super::cooperative_sched_target(pid)) diff --git a/kernel/src/syscall/wait.rs b/kernel/src/syscall/wait.rs index 8a8baa3..3db30ba 100644 --- a/kernel/src/syscall/wait.rs +++ b/kernel/src/syscall/wait.rs @@ -45,8 +45,15 @@ pub(crate) fn futex_waiter_present(addr: u64, pid: u32) -> bool { } pub(crate) fn futex_waiter_for(addr: u64, exclude: u32) -> Option { + // Only return a waiter in the SAME address space as the caller (`exclude` is + // the calling pid). The waiter table is keyed by VA alone, which collides + // across processes that load the same .so at the same address — see + // futex_wake_waiters for the full rationale. + let grp = if exclude == 0 { 0 } else { crate::process::get_group_leader(exclude) }; let table = FUTEX_WAITERS.lock(); table.get(&addr).and_then(|waiters| { - waiters.iter().copied().find(|&pid| pid != exclude) + waiters.iter().copied().find(|&pid| { + pid != exclude && (grp == 0 || crate::process::get_group_leader(pid) == grp) + }) }) } diff --git a/kernel/src/wm/mod.rs b/kernel/src/wm/mod.rs index fe78126..b294c5a 100644 --- a/kernel/src/wm/mod.rs +++ b/kernel/src/wm/mod.rs @@ -22,9 +22,16 @@ fn canonical_pid(pid: u32) -> u32 { } } +/// Visibility test between an event's owner and a consumer. BOTH pids MUST be +/// already canonical (group leaders). This is a RAW comparison that deliberately +/// does NOT call `canonical_pid` — that would take PTABLE_LOCK, and this runs +/// inside `with_queue` (WM lock held). Acquiring PTABLE_LOCK under the WM lock +/// inverts the scheduler's PTABLE→WM order (next_runnable_pid_locked calls +/// input_pending_for while holding PTABLE_LOCK) → ABBA deadlock under SMP. +/// Callers canonicalize at the WM-module boundary, before taking the WM lock. #[inline(always)] fn owner_visible_to_consumer(owner_pid: u32, consumer_pid: u32) -> bool { - owner_pid == 0 || owner_pid == canonical_pid(consumer_pid) + owner_pid == 0 || owner_pid == consumer_pid } struct EventQueue { @@ -51,7 +58,10 @@ impl EventQueue { } fn push(&mut self, mut ev: WmEvent, owner_pid: u32) { - let owner_pid = canonical_pid(owner_pid); + // owner_pid is ALREADY canonical (canonicalized by the caller before the + // WM lock was taken). Do NOT call canonical_pid here — it takes PTABLE_LOCK + // under the WM lock and deadlocks against the scheduler (see + // owner_visible_to_consumer). ev.seq = self.next_seq; self.next_seq = self.next_seq.wrapping_add(1).max(1); @@ -105,7 +115,7 @@ impl EventQueue { } fn push_front(&mut self, mut ev: WmEvent, owner_pid: u32) { - let owner_pid = canonical_pid(owner_pid); + // owner_pid is ALREADY canonical (see push() — never take PTABLE_LOCK here). ev.seq = self.next_seq; self.next_seq = self.next_seq.wrapping_add(1).max(1); @@ -524,9 +534,12 @@ pub fn push_event_for(owner_pid: u32, kind: u32, flags: u32, a: u64, b: u64) { ); }); - // Wake the waiter if they are eligible for this event. + // Wake the waiter if they are eligible for this event. We are now OUTSIDE the + // WM lock (the with_queue closure closed above), so canonicalizing the raw + // waiter pid here is safe — owner_visible_to_consumer itself is raw and must + // be fed canonical pids (owner_pid was canonicalized at fn entry). let waiter = WM_WAITER.load(Ordering::Acquire); - if waiter != 0 && owner_visible_to_consumer(owner_pid, waiter) { + if waiter != 0 && owner_visible_to_consumer(owner_pid, canonical_pid(waiter)) { WM_WAITER.store(0, Ordering::Release); crate::process::wake_process(waiter); } @@ -636,6 +649,59 @@ static CURSOR_BUTTONS: AtomicU32 = AtomicU32::new(0); static CURSOR_SEEN: core::sync::atomic::AtomicBool = core::sync::atomic::AtomicBool::new(false); static CURSOR_LAST_ACT_NS: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0); +// ── Input introspection ────────────────────────────────────────────────────── +// +// Lightweight, always-on telemetry: which pointing/key device bound, and how many +// events each kind has delivered. Cheap (a few relaxed atomics per event) and +// genuinely useful for answering "is any input device live, and is it sending +// events?" — the exact question when a board/VM shows no cursor. The optional +// `input-hud` build feature draws these on-screen (see compositor::input_hud); the +// counters themselves are unconditional so the data exists even without the HUD. + +/// Which input device last bound a pointer/key source. +pub const INPUT_SRC_NONE: u8 = 0; +pub const INPUT_SRC_VIRTIO: u8 = 1; +pub const INPUT_SRC_XHCI: u8 = 2; +pub const INPUT_SRC_PS2: u8 = 3; + +static INPUT_SOURCE: core::sync::atomic::AtomicU8 = core::sync::atomic::AtomicU8::new(INPUT_SRC_NONE); +static PTR_COUNT: AtomicU32 = AtomicU32::new(0); +static KEY_COUNT: AtomicU32 = AtomicU32::new(0); +static SCROLL_COUNT: AtomicU32 = AtomicU32::new(0); +/// Last key event, packed `(scancode << 1) | pressed`. +static LAST_KEY: AtomicU32 = AtomicU32::new(0); + +/// Record that an input device has bound (called once per device from its driver). +pub fn set_input_source(src: u8) { + INPUT_SOURCE.store(src, Ordering::Relaxed); +} +/// The bound input source (`INPUT_SRC_*`), or `INPUT_SRC_NONE` if nothing bound. +pub fn input_source() -> u8 { + INPUT_SOURCE.load(Ordering::Relaxed) +} +/// Short name for the bound input source. +pub fn input_source_name() -> &'static str { + match input_source() { + INPUT_SRC_VIRTIO => "virtio", + INPUT_SRC_XHCI => "xhci", + INPUT_SRC_PS2 => "ps2", + _ => "none", + } +} +/// Event counts so far: `(pointer, key, scroll)`. +pub fn input_counts() -> (u32, u32, u32) { + ( + PTR_COUNT.load(Ordering::Relaxed), + KEY_COUNT.load(Ordering::Relaxed), + SCROLL_COUNT.load(Ordering::Relaxed), + ) +} +/// Last key event as `(scancode, pressed)`. +pub fn last_key() -> (u32, bool) { + let v = LAST_KEY.load(Ordering::Relaxed); + (v >> 1, (v & 1) != 0) +} + /// Current pointer position (absolute, framebuffer pixels). pub fn cursor_pos() -> (i32, i32) { (CURSOR_X.load(Ordering::Relaxed), CURSOR_Y.load(Ordering::Relaxed)) @@ -660,6 +726,7 @@ pub fn push_pointer(x: i32, y: i32, buttons: u32) { CURSOR_BUTTONS.store(buttons, Ordering::Relaxed); CURSOR_SEEN.store(true, Ordering::Relaxed); CURSOR_LAST_ACT_NS.store(crate::arch::rdtsc_ns(), Ordering::Relaxed); + PTR_COUNT.fetch_add(1, Ordering::Relaxed); let packed = ((x as u32 as u64) << 32) | (y as u32 as u64); @@ -696,6 +763,7 @@ pub fn push_pointer(x: i32, y: i32, buttons: u32) { /// Deliver a mouse scroll-wheel tick. `dz` is the signed wheel delta /// (negative = scroll toward the user / content down, positive = away / up). pub fn push_scroll(x: i32, y: i32, dz: i32) { + SCROLL_COUNT.fetch_add(1, Ordering::Relaxed); let packed = ((x as u32 as u64) << 32) | (y as u32 as u64); let dz_bits = dz as u32 as u64; // preserve sign through i32→u32→u64 @@ -716,6 +784,8 @@ pub fn push_scroll(x: i32, y: i32, dz: i32) { } pub fn push_key(scancode: u32, pressed: bool) { + KEY_COUNT.fetch_add(1, Ordering::Relaxed); + LAST_KEY.store((scancode << 1) | (pressed as u32), Ordering::Relaxed); // F2 (PS/2 set-1 make 0x3C) toggles the kernel boot screen's verbose log // overlay. Handled here so it works during the engine warm-up before any app // is focused; the keypress is still forwarded for normal handling. diff --git a/packages/oscortex_net/README.md b/packages/oscortex_net/README.md new file mode 100644 index 0000000..af80bc9 --- /dev/null +++ b/packages/oscortex_net/README.md @@ -0,0 +1,45 @@ +# oscortex_net + +App networking for OSCortex userspace Flutter apps — a TCP socket plus a minimal +plaintext HTTP client, over the `oscortex/net` platform channel into the kernel +TCP stack. + +## Usage + +```dart +import 'package:oscortex_net/oscortex_net.dart'; + +// Raw TCP +final sock = await OscortexSocket.connect('10.0.2.2', 8080); +await sock.writeAll('ping'.codeUnits); +final reply = await sock.read(); +await sock.close(); + +// Minimal HTTP GET (plaintext / port 80; pass the server IP for now) +final body = await httpGet('93.184.216.34', 'example.com', '/'); +``` + +`OscortexSocket.connect` returns only once the (asynchronous) kernel handshake +has completed, so you can write immediately. + +## Status + +| Capability | State | +|---|---| +| TCP connect / status / send / recv / close | ✅ (kernel syscalls + embedder channel) | +| Plaintext HTTP/1.0 GET | ✅ helper | +| DNS by hostname | ⏳ next increment (pass an IP for now) | +| TLS / HTTPS | ⏳ (smoltcp has no TLS; needs a userspace TLS layer) | + +## How it works + +``` +Dart (this package) + └─ BasicMessageChannel('oscortex/net', BinaryCodec()) compact opcode frames + └─ embedder handle_net_channel (tools/flutter-embedder/src/main.rs) + └─ TCP syscalls 0x388-0x38B + tcp_status 0x4B7 (CAP_NET) + └─ net::tcp (smoltcp) — the same stack the package pipeline uses +``` + +Apps are granted `CAP_NET` by the kernel so these calls are permitted (the +security phase will make that a per-app declared/prompted permission). diff --git a/packages/oscortex_net/lib/oscortex_net.dart b/packages/oscortex_net/lib/oscortex_net.dart new file mode 100644 index 0000000..a355d87 --- /dev/null +++ b/packages/oscortex_net/lib/oscortex_net.dart @@ -0,0 +1,240 @@ +/// OSCortex app networking. +/// +/// A thin TCP socket API for userspace Flutter apps, plus a minimal plaintext +/// HTTP client built on top of it. Everything funnels through the `oscortex/net` +/// platform channel (a `BasicMessageChannel` with the binary codec, so +/// TCP byte streams pass through unmangled) into the kernel TCP syscalls. +/// +/// The kernel `tcp_connect` is asynchronous, so [OscortexSocket.connect] polls +/// the connection status until the handshake completes before returning. +/// +/// Not yet supported (follow-up increments): DNS by hostname (pass an IP for +/// now) and TLS (so HTTP only, not HTTPS). +library oscortex_net; + +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:flutter/services.dart'; + +/// Raised when a networking operation fails; [code] is the kernel errno +/// (negative) or a client-side sentinel. +class OscortexNetException implements Exception { + const OscortexNetException(this.message, this.code); + final String message; + final int code; + @override + String toString() => 'OscortexNetException($code): $message'; +} + +/// A connected TCP socket backed by a kernel socket fd. +class OscortexSocket { + OscortexSocket._(this.fd); + + /// Kernel socket fd. + final int fd; + + static const BasicMessageChannel _channel = + BasicMessageChannel('oscortex/net', BinaryCodec()); + + // Opcodes (must match the embedder's handle_net_channel). + static const int _opConnect = 0x01; + static const int _opStatus = 0x02; + static const int _opSend = 0x03; + static const int _opRecv = 0x04; + static const int _opClose = 0x05; + static const int _opResolve = 0x06; + + /// Resolve [host] to a dotted-quad IPv4 string, or null on failure. + /// (DNS A-record lookup via the kernel resolver.) + static Future resolve(String host) async { + final List name = host.codeUnits; + final Uint8List req = Uint8List(1 + name.length)..[0] = _opResolve; + req.setRange(1, 1 + name.length, name); + final ByteData? reply = await _channel.send(ByteData.sublistView(req)); + if (reply == null || reply.lengthInBytes < 4) return null; + // BE-order IPv4 as an unsigned u32; 0 = failure sentinel. + final int v = reply.getUint32(0, Endian.little); + if (v == 0) return null; + return '${(v >> 24) & 0xFF}.${(v >> 16) & 0xFF}.${(v >> 8) & 0xFF}.${v & 0xFF}'; + } + + /// Send a request frame, return the leading little-endian i32 result. + static Future _callI32(Uint8List req) async { + final ByteData? reply = await _channel.send(ByteData.sublistView(req)); + if (reply == null || reply.lengthInBytes < 4) return -22; // EINVAL + return reply.getInt32(0, Endian.little); + } + + static void _putFd(Uint8List req, int fd) { + req[1] = fd & 0xFF; + req[2] = (fd >> 8) & 0xFF; + req[3] = (fd >> 16) & 0xFF; + req[4] = (fd >> 24) & 0xFF; + } + + static int _parseIpv4(String ip) { + final List parts = ip.split('.'); + if (parts.length != 4) { + throw OscortexNetException('not an IPv4 address: $ip', -22); + } + var v = 0; + for (final String p in parts) { + final int o = int.tryParse(p) ?? -1; + if (o < 0 || o > 255) { + throw OscortexNetException('not an IPv4 address: $ip', -22); + } + v = (v << 8) | o; + } + return v & 0xFFFFFFFF; + } + + /// Open a TCP connection to [ip] (dotted-quad) : [port], returning once the + /// handshake has completed. Throws [OscortexNetException] on failure/timeout. + static Future connect( + String ip, + int port, { + Duration timeout = const Duration(seconds: 10), + }) async { + final int ipBe = _parseIpv4(ip); + final Uint8List req = Uint8List(7); + req[0] = _opConnect; + req[1] = (ipBe >> 24) & 0xFF; + req[2] = (ipBe >> 16) & 0xFF; + req[3] = (ipBe >> 8) & 0xFF; + req[4] = ipBe & 0xFF; + req[5] = (port >> 8) & 0xFF; + req[6] = port & 0xFF; + + final int fd = await _callI32(req); + if (fd < 0) throw OscortexNetException('connect failed', fd); + + final OscortexSocket sock = OscortexSocket._(fd); + final DateTime deadline = DateTime.now().add(timeout); + while (true) { + final int st = await sock._status(); + if (st == 1) return sock; // established + if (st < 0) { + await sock.close(); + throw OscortexNetException('connection refused/reset', st); + } + if (DateTime.now().isAfter(deadline)) { + await sock.close(); + throw const OscortexNetException('connect timed out', -110); + } + await Future.delayed(const Duration(milliseconds: 20)); + } + } + + Future _status() { + final Uint8List req = Uint8List(5)..[0] = _opStatus; + _putFd(req, fd); + return _callI32(req); + } + + /// Write [data], retrying EAGAIN internally. Returns bytes written (which may + /// be fewer than `data.length` — use [writeAll] to send everything). + Future write( + List data, { + Duration timeout = const Duration(seconds: 5), + }) async { + final Uint8List req = Uint8List(5 + data.length); + req[0] = _opSend; + _putFd(req, fd); + req.setRange(5, 5 + data.length, data); + + final DateTime deadline = DateTime.now().add(timeout); + while (true) { + final int n = await _callI32(req); + if (n != -11) { + // not EAGAIN + if (n < 0) throw OscortexNetException('write failed', n); + return n; + } + if (DateTime.now().isAfter(deadline)) { + throw const OscortexNetException('write timed out', -110); + } + await Future.delayed(const Duration(milliseconds: 10)); + } + } + + /// Write every byte of [data], looping until the buffer is drained. + Future writeAll(List data) async { + var off = 0; + while (off < data.length) { + off += await write(data.sublist(off)); + } + } + + /// Read up to [max] bytes. Returns the bytes received, or an empty list if no + /// data arrived within [timeout] (e.g. the peer closed or is idle). The caller + /// loops until it has what it needs (see [httpGet]). + Future read({ + int max = 8192, + Duration timeout = const Duration(seconds: 2), + }) async { + final Uint8List req = Uint8List(9)..[0] = _opRecv; + _putFd(req, fd); + req.buffer.asByteData().setUint32(5, max, Endian.little); + + final DateTime deadline = DateTime.now().add(timeout); + while (true) { + final ByteData? reply = await _channel.send(ByteData.sublistView(req)); + if (reply == null || reply.lengthInBytes < 4) { + throw const OscortexNetException('read failed', -5); + } + final int n = reply.getInt32(0, Endian.little); + if (n > 0) { + // Copy out of the channel buffer (don't alias it). + return Uint8List.fromList( + reply.buffer.asUint8List(reply.offsetInBytes + 4, n), + ); + } + if (n < 0 && n != -11) { + throw OscortexNetException('read failed', n); + } + // n == 0 (nothing this poll) or -11 (EAGAIN): wait and retry until deadline. + if (DateTime.now().isAfter(deadline)) return Uint8List(0); + await Future.delayed(const Duration(milliseconds: 20)); + } + } + + /// Close the socket. + Future close() async { + final Uint8List req = Uint8List(5)..[0] = _opClose; + _putFd(req, fd); + await _callI32(req); + } +} + +/// Minimal plaintext HTTP/1.0 GET over a raw TCP socket (no TLS yet, so HTTP +/// only). [ip] is the server's dotted-quad address, [host] the Host header, +/// [path] the request path. Returns the response body (headers stripped). +/// +/// Until DNS lands, resolve the host to an IP out-of-band and pass it here. +Future httpGet( + String ip, + String host, + String path, { + int port = 80, +}) async { + final OscortexSocket sock = await OscortexSocket.connect(ip, port); + try { + final String request = + 'GET $path HTTP/1.0\r\nHost: $host\r\nConnection: close\r\n\r\n'; + await sock.writeAll(request.codeUnits); + + final BytesBuilder body = BytesBuilder(); + while (true) { + final Uint8List chunk = await sock.read(); + if (chunk.isEmpty) break; // peer closed / idle past the read timeout + body.add(chunk); + } + + final String text = String.fromCharCodes(body.takeBytes()); + final int sep = text.indexOf('\r\n\r\n'); + return sep >= 0 ? text.substring(sep + 4) : text; + } finally { + await sock.close(); + } +} diff --git a/packages/oscortex_net/pubspec.yaml b/packages/oscortex_net/pubspec.yaml new file mode 100644 index 0000000..90e659c --- /dev/null +++ b/packages/oscortex_net/pubspec.yaml @@ -0,0 +1,19 @@ +name: oscortex_net +description: > + OSCortex app networking — a TCP socket + minimal HTTP client for userspace + Flutter apps, over the `oscortex/net` platform channel into the kernel TCP + stack. (DNS by hostname lands in a follow-up; pass an IP for now.) +version: 0.1.0 +publish_to: none + +environment: + sdk: ^3.11.0 + flutter: '>=3.29.0' + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter diff --git a/packages/oscortex_net/test/oscortex_net_test.dart b/packages/oscortex_net/test/oscortex_net_test.dart new file mode 100644 index 0000000..16222b0 --- /dev/null +++ b/packages/oscortex_net/test/oscortex_net_test.dart @@ -0,0 +1,197 @@ +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:oscortex_net/oscortex_net.dart'; + +/// Verifies the `oscortex/net` wire protocol from the Dart side: every request +/// frame OscortexSocket emits must match what the embedder's handle_net_channel +/// parses, and replies must be decoded correctly. The kernel/embedder is mocked +/// via the binary messenger, so these run without a device. +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + const String channelName = 'oscortex/net'; + + final List sent = []; + // Tunable canned recv reply (overridden per test). + late int recvN; + late List recvData; + + Uint8List bytesOf(ByteData? m) => + m!.buffer.asUint8List(m.offsetInBytes, m.lengthInBytes); + ByteData i32(int v) => ByteData(4)..setInt32(0, v, Endian.little); + + TestDefaultBinaryMessenger messenger() => + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger; + + setUp(() { + sent.clear(); + recvN = 2; + recvData = 'hi'.codeUnits; + messenger().setMockMessageHandler(channelName, (ByteData? message) async { + final Uint8List f = bytesOf(message); + sent.add(f); + switch (f[0]) { + case 0x01: // connect → fd 3 + return i32(3); + case 0x02: // status → established + return i32(1); + case 0x03: // send → bytes written == payload length + return i32(f.length - 5); + case 0x04: // recv → [i32 n | data] + final int body = recvN > 0 ? recvData.length : 0; + final ByteData r = ByteData(4 + body); + r.setInt32(0, recvN, Endian.little); + for (int i = 0; i < body; i++) { + r.setUint8(4 + i, recvData[i]); + } + return r; + case 0x05: // close → 0 + return i32(0); + default: + return i32(-22); + } + }); + }); + + tearDown(() => messenger().setMockMessageHandler(channelName, null)); + + test('connect sends [op, ip big-endian, port big-endian] and yields the fd', + () async { + final OscortexSocket sock = await OscortexSocket.connect('10.0.2.2', 8099); + expect(sock.fd, 3); + + final Uint8List c = sent.first; + expect(c[0], 0x01); + expect([c[1], c[2], c[3], c[4]], [10, 0, 2, 2]); // ip BE + expect([c[5], c[6]], [(8099 >> 8) & 0xFF, 8099 & 0xFF]); // port BE + // connect must poll status (op 0x02) until established before returning. + expect(sent.any((Uint8List f) => f[0] == 0x02), isTrue); + }); + + test('connect throws on a malformed IP (before sending anything)', () async { + await expectLater( + OscortexSocket.connect('not.an.ip', 80), + throwsA(isA()), + ); + expect(sent, isEmpty); + }); + + test('connect throws when the kernel returns a negative fd', () async { + messenger().setMockMessageHandler(channelName, (ByteData? m) async => i32(-23)); + await expectLater( + OscortexSocket.connect('10.0.2.2', 80), + throwsA(isA()), + ); + }); + + test('write sends [op, fd little-endian, data] and returns bytes written', + () async { + final OscortexSocket sock = await OscortexSocket.connect('10.0.2.2', 80); + sent.clear(); + + final int n = await sock.write([1, 2, 3, 4]); + expect(n, 4); + + final Uint8List w = sent.single; + expect(w[0], 0x03); + expect([w[1], w[2], w[3], w[4]], [3, 0, 0, 0]); // fd=3 LE + expect(w.sublist(5), [1, 2, 3, 4]); + }); + + test('read sends [op, fd LE, max LE] and returns the payload bytes', () async { + final OscortexSocket sock = await OscortexSocket.connect('10.0.2.2', 80); + sent.clear(); + recvN = 5; + recvData = 'hello'.codeUnits; + + final Uint8List data = await sock.read(max: 4096); + expect(String.fromCharCodes(data), 'hello'); + + final Uint8List r = sent.first; + expect(r[0], 0x04); + expect([r[1], r[2], r[3], r[4]], [3, 0, 0, 0]); // fd LE + final int maxField = r[5] | (r[6] << 8) | (r[7] << 16) | (r[8] << 24); + expect(maxField, 4096); // max, little-endian + }); + + test('read returns empty once the timeout elapses with no data', () async { + final OscortexSocket sock = await OscortexSocket.connect('10.0.2.2', 80); + recvN = 0; // kernel: nothing available this poll + final Uint8List data = + await sock.read(timeout: const Duration(milliseconds: 80)); + expect(data, isEmpty); + }); + + test('close sends [op, fd little-endian]', () async { + final OscortexSocket sock = await OscortexSocket.connect('10.0.2.2', 80); + sent.clear(); + await sock.close(); + + final Uint8List f = sent.single; + expect(f[0], 0x05); + expect([f[1], f[2], f[3], f[4]], [3, 0, 0, 0]); + }); + + test('httpGet sends a GET request and returns the body with headers stripped', + () async { + const String response = 'HTTP/1.0 200 OK\r\n' + 'Content-Type: text/plain\r\n' + 'Content-Length: 5\r\n' + '\r\n' + 'HELLO'; + int reads = 0; + messenger().setMockMessageHandler(channelName, (ByteData? message) async { + final Uint8List f = bytesOf(message); + sent.add(f); + switch (f[0]) { + case 0x01: + return i32(7); // connect → fd 7 + case 0x02: + return i32(1); // established + case 0x03: + return i32(f.length - 5); // wrote the whole request + case 0x04: + if (reads++ == 0) { + final List b = response.codeUnits; + final ByteData r = ByteData(4 + b.length); + r.setInt32(0, b.length, Endian.little); + for (int i = 0; i < b.length; i++) { + r.setUint8(4 + i, b[i]); + } + return r; + } + return i32(0); // no more data → httpGet's read loop ends on timeout + case 0x05: + return i32(0); + default: + return i32(-22); + } + }); + + final String body = await httpGet('93.184.216.34', 'example.com', '/'); + expect(body, 'HELLO'); + + final Uint8List writeFrame = sent.firstWhere((Uint8List f) => f[0] == 0x03); + final String reqText = String.fromCharCodes(writeFrame.sublist(5)); + expect(reqText, startsWith('GET / HTTP/1.0\r\n')); + expect(reqText, contains('Host: example.com\r\n')); + expect(reqText, contains('Connection: close')); + }, timeout: const Timeout(Duration(seconds: 15))); + + test('resolve sends [op, hostname] and parses the u32 IPv4 reply', () async { + messenger().setMockMessageHandler(channelName, (ByteData? message) async { + final Uint8List f = bytesOf(message); + expect(f[0], 0x06); + expect(String.fromCharCodes(f.sublist(1)), 'example.com'); + final int v = (93 << 24) | (184 << 16) | (216 << 8) | 34; // 93.184.216.34 + return ByteData(4)..setUint32(0, v, Endian.little); + }); + expect(await OscortexSocket.resolve('example.com'), '93.184.216.34'); + }); + + test('resolve returns null on the 0 failure sentinel', () async { + messenger().setMockMessageHandler(channelName, + (ByteData? m) async => ByteData(4)..setUint32(0, 0, Endian.little)); + expect(await OscortexSocket.resolve('nope.invalid'), isNull); + }); +} diff --git a/packages/oscortex_ui/.dart_tool/extension_discovery/README.md b/packages/oscortex_ui/.dart_tool/extension_discovery/README.md deleted file mode 100644 index 9dc6757..0000000 --- a/packages/oscortex_ui/.dart_tool/extension_discovery/README.md +++ /dev/null @@ -1,31 +0,0 @@ -Extension Discovery Cache -========================= - -This folder is used by `package:extension_discovery` to cache lists of -packages that contains extensions for other packages. - -DO NOT USE THIS FOLDER ----------------------- - - * Do not read (or rely) the contents of this folder. - * Do write to this folder. - -If you're interested in the lists of extensions stored in this folder use the -API offered by package `extension_discovery` to get this information. - -If this package doesn't work for your use-case, then don't try to read the -contents of this folder. It may change, and will not remain stable. - -Use package `extension_discovery` ---------------------------------- - -If you want to access information from this folder. - -Feel free to delete this folder -------------------------------- - -Files in this folder act as a cache, and the cache is discarded if the files -are older than the modification time of `.dart_tool/package_config.json`. - -Hence, it should never be necessary to clear this cache manually, if you find a -need to do please file a bug. diff --git a/packages/oscortex_ui/.dart_tool/extension_discovery/devtools.json b/packages/oscortex_ui/.dart_tool/extension_discovery/devtools.json deleted file mode 100644 index 9057020..0000000 --- a/packages/oscortex_ui/.dart_tool/extension_discovery/devtools.json +++ /dev/null @@ -1 +0,0 @@ -{"version":2,"entries":[{"package":"oscortex_ui","rootUri":"../","packageUri":"lib/"}]} \ No newline at end of file diff --git a/packages/oscortex_ui/.dart_tool/extension_discovery/vs_code.json b/packages/oscortex_ui/.dart_tool/extension_discovery/vs_code.json deleted file mode 100644 index 9057020..0000000 --- a/packages/oscortex_ui/.dart_tool/extension_discovery/vs_code.json +++ /dev/null @@ -1 +0,0 @@ -{"version":2,"entries":[{"package":"oscortex_ui","rootUri":"../","packageUri":"lib/"}]} \ No newline at end of file diff --git a/packages/oscortex_ui/.dart_tool/package_config.json b/packages/oscortex_ui/.dart_tool/package_config.json deleted file mode 100644 index ba0e83e..0000000 --- a/packages/oscortex_ui/.dart_tool/package_config.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "configVersion": 2, - "packages": [ - { - "name": "characters", - "rootUri": "file:///Users/ghostportal/.pub-cache/hosted/pub.dev/characters-1.4.1", - "packageUri": "lib/", - "languageVersion": "3.4" - }, - { - "name": "collection", - "rootUri": "file:///Users/ghostportal/.pub-cache/hosted/pub.dev/collection-1.19.1", - "packageUri": "lib/", - "languageVersion": "3.4" - }, - { - "name": "flutter", - "rootUri": "file:///opt/homebrew/share/flutter/packages/flutter", - "packageUri": "lib/", - "languageVersion": "3.9" - }, - { - "name": "material_color_utilities", - "rootUri": "file:///Users/ghostportal/.pub-cache/hosted/pub.dev/material_color_utilities-0.13.0", - "packageUri": "lib/", - "languageVersion": "3.5" - }, - { - "name": "meta", - "rootUri": "file:///Users/ghostportal/.pub-cache/hosted/pub.dev/meta-1.17.0", - "packageUri": "lib/", - "languageVersion": "3.5" - }, - { - "name": "sky_engine", - "rootUri": "file:///opt/homebrew/share/flutter/bin/cache/pkg/sky_engine", - "packageUri": "lib/", - "languageVersion": "3.9" - }, - { - "name": "vector_math", - "rootUri": "file:///Users/ghostportal/.pub-cache/hosted/pub.dev/vector_math-2.2.0", - "packageUri": "lib/", - "languageVersion": "3.1" - }, - { - "name": "oscortex_ui", - "rootUri": "../", - "packageUri": "lib/", - "languageVersion": "3.11" - } - ], - "generator": "pub", - "generatorVersion": "3.11.0", - "flutterRoot": "file:///opt/homebrew/share/flutter", - "flutterVersion": "3.41.1", - "pubCache": "file:///Users/ghostportal/.pub-cache" -} diff --git a/packages/oscortex_ui/.dart_tool/package_graph.json b/packages/oscortex_ui/.dart_tool/package_graph.json deleted file mode 100644 index b56e5e6..0000000 --- a/packages/oscortex_ui/.dart_tool/package_graph.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "roots": [ - "oscortex_ui" - ], - "packages": [ - { - "name": "oscortex_ui", - "version": "0.1.0", - "dependencies": [ - "flutter" - ], - "devDependencies": [] - }, - { - "name": "flutter", - "version": "0.0.0", - "dependencies": [ - "characters", - "collection", - "material_color_utilities", - "meta", - "sky_engine", - "vector_math" - ] - }, - { - "name": "sky_engine", - "version": "0.0.0", - "dependencies": [] - }, - { - "name": "vector_math", - "version": "2.2.0", - "dependencies": [] - }, - { - "name": "meta", - "version": "1.17.0", - "dependencies": [] - }, - { - "name": "material_color_utilities", - "version": "0.13.0", - "dependencies": [ - "collection" - ] - }, - { - "name": "collection", - "version": "1.19.1", - "dependencies": [] - }, - { - "name": "characters", - "version": "1.4.1", - "dependencies": [] - } - ], - "configVersion": 1 -} \ No newline at end of file diff --git a/packages/oscortex_ui/.dart_tool/version b/packages/oscortex_ui/.dart_tool/version deleted file mode 100644 index 050c055..0000000 --- a/packages/oscortex_ui/.dart_tool/version +++ /dev/null @@ -1 +0,0 @@ -3.41.1 \ No newline at end of file diff --git a/packages/oscortex_webview/lib/oscortex_webview.dart b/packages/oscortex_webview/lib/oscortex_webview.dart new file mode 100644 index 0000000..2c37a5a --- /dev/null +++ b/packages/oscortex_webview/lib/oscortex_webview.dart @@ -0,0 +1,322 @@ +/// OSCortex native webview for userspace Flutter apps. +/// +/// A userspace web-engine service (Servo) renders a page into its own OSCortex +/// compositor surface, which the kernel z-stacks directly beneath the app's chrome +/// and clips to the viewport. This package is the app-side half: +/// +/// * [OscWebViewController] drives navigation and receives events over the +/// `oscortex/webview` [MethodChannel]. +/// * [OscWebView] is a transparent placeholder widget that reserves the web +/// region, reports its on-screen rect (so the kernel can position + clip the +/// web surface), and forwards pointer/scroll/key input to the engine. +/// +/// Flutter's external-texture API is GPU-only and OSCortex is software-rendered, so +/// there is deliberately no texture here — the web pixels are a sibling OS surface +/// showing through a transparent hole in the Flutter scene. The app must therefore +/// keep the [OscWebView] region transparent (don't paint an opaque background over +/// it) so the lower surface is visible. +/// +/// The contract is engine-agnostic: a stub service or Servo serve it identically. +library oscortex_webview; + +import 'dart:async'; + +import 'package:flutter/gestures.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; + +/// Pointer/key event `type` codes sent in [OscWebViewController.dispatchInput] / +/// [OscWebViewController.dispatchKey]. Mirror the kernel's EV_* input vocabulary. +class OscInput { + OscInput._(); + static const int pointerDown = 0; + static const int pointerMove = 1; + static const int pointerUp = 2; + static const int pointerHover = 3; + static const int keyDown = 0; + static const int keyUp = 1; +} + +/// Drives one native webview instance over `oscortex/webview`. +/// +/// Construct with a unique [viewId]; call [create] once the render size is known, +/// then [loadUrl] / [goBack] / etc. Assign the `on*` callbacks to observe engine +/// events. Call [dispose] to stop receiving events and free the registry slot. +class OscWebViewController { + OscWebViewController(this.viewId) { + _instances[viewId] = this; + if (!_handlerInstalled) { + _channel.setMethodCallHandler(_dispatch); + _handlerInstalled = true; + } + } + + /// Per-instance id, included in every call/event for multiplexing. + final int viewId; + + /// The channel name (shared by all instances; events are routed by `viewId`). + static const String channelName = 'oscortex/webview'; + static const MethodChannel _channel = MethodChannel(channelName); + + static final Map _instances = + {}; + static bool _handlerInstalled = false; + + // ── Engine → app event callbacks ──────────────────────────────────────────── + void Function(String url)? onUrlChanged; + void Function(String title)? onTitleChanged; + void Function(int progress)? onProgress; // 0..100 + void Function(String url)? onLoadStarted; + void Function(String url)? onLoadFinished; + void Function(int code, String description, String url)? onLoadError; + void Function(bool canGoBack, bool canGoForward)? onNavState; + void Function(int x, int y)? onScrollChanged; + + Map _a([Map? extra]) => + {'viewId': viewId, ...?extra}; + + // ── App → engine methods ──────────────────────────────────────────────────── + + /// Invoke a channel method, tolerating the absence of an engine service: + /// before the `oscortex/webview` service binds (scaffold phase, or if it + /// crashed) `invokeMethod` raises [MissingPluginException]; we treat that as + /// "no engine yet" and return null so callers (incl. the widget) stay inert + /// rather than throwing. Any other error propagates. + Future _invoke(String method, [Map? extra]) async { + try { + return await _channel.invokeMethod(method, _a(extra)); + } on MissingPluginException { + return null; + } + } + + /// Create the engine's render surface; returns the OSCortex `surfaceId` + /// (or -1 if no engine service is bound yet). + Future create({ + required int width, + required int height, + double dpr = 1.0, + String? initialUrl, + }) async { + try { + final Map? r = + await _channel.invokeMapMethod( + 'create', + _a({ + 'width': width, + 'height': height, + 'dpr': dpr, + 'initialUrl': initialUrl, + }), + ); + return (r?['surfaceId'] as int?) ?? -1; + } on MissingPluginException { + return -1; + } + } + + Future destroy() => _invoke('destroy'); + + Future loadUrl(String url, {Map? headers}) => + _invoke('loadUrl', {'url': url, 'headers': headers}); + + Future loadHtml(String html, {String? baseUrl}) => + _invoke('loadHtml', {'html': html, 'baseUrl': baseUrl}); + + Future reload() => _invoke('reload'); + Future stopLoading() => _invoke('stopLoading'); + Future goBack() => _invoke('goBack'); + Future goForward() => _invoke('goForward'); + + Future canGoBack() async => (await _invoke('canGoBack')) ?? false; + Future canGoForward() async => + (await _invoke('canGoForward')) ?? false; + + Future currentUrl() => _invoke('currentUrl'); + Future getTitle() => _invoke('getTitle'); + + Future evalJs(String source) => + _invoke('evalJs', {'source': source}); + + Future resize({required int width, required int height, double dpr = 1.0}) => + _invoke('resize', + {'width': width, 'height': height, 'dpr': dpr}); + + /// Tell the engine/kernel where the webview rect lives on screen + its scroll + /// offset, so the web surface can be positioned and clipped to it. + Future setViewport({ + required int x, + required int y, + required int w, + required int h, + int scrollDx = 0, + int scrollDy = 0, + }) => + _invoke('setViewport', { + 'x': x, + 'y': y, + 'w': w, + 'h': h, + 'scrollDx': scrollDx, + 'scrollDy': scrollDy, + }); + + /// Forward a pointer event in webview-local coordinates. [type] is an + /// `OscInput.pointer*` code; [button] a button bitmask; [modifiers] a key-mod mask. + Future dispatchInput({ + required int type, + required double x, + required double y, + int button = 0, + int modifiers = 0, + }) => + _invoke('dispatchInput', { + 'type': type, + 'x': x, + 'y': y, + 'button': button, + 'modifiers': modifiers, + }); + + Future dispatchScroll({ + required double x, + required double y, + required double dx, + required double dy, + }) => + _invoke('dispatchScroll', + {'x': x, 'y': y, 'dx': dx, 'dy': dy}); + + Future dispatchKey({ + required int type, + required int keyCode, + int modifiers = 0, + String? text, + }) => + _invoke('dispatchKey', { + 'type': type, + 'keyCode': keyCode, + 'modifiers': modifiers, + 'text': text, + }); + + Future setVisible(bool visible) => + _invoke('setVisible', {'visible': visible}); + + /// Stop receiving events for this instance. + void dispose() { + _instances.remove(viewId); + } + + // ── Event routing (one channel handler fans out to instances by viewId) ────── + static Future _dispatch(MethodCall call) async { + final Map a = + (call.arguments as Map?)?.cast() ?? + const {}; + _instances[a['viewId'] as int?]?._onEvent(call.method, a); + return null; + } + + void _onEvent(String method, Map a) { + switch (method) { + case 'urlChanged': + onUrlChanged?.call(a['url'] as String? ?? ''); + case 'titleChanged': + onTitleChanged?.call(a['title'] as String? ?? ''); + case 'loadProgress': + onProgress?.call(a['progress'] as int? ?? 0); + case 'loadStarted': + onLoadStarted?.call(a['url'] as String? ?? ''); + case 'loadFinished': + onLoadFinished?.call(a['url'] as String? ?? ''); + case 'loadError': + onLoadError?.call(a['code'] as int? ?? 0, + a['description'] as String? ?? '', a['url'] as String? ?? ''); + case 'navState': + onNavState?.call( + a['canGoBack'] as bool? ?? false, a['canGoForward'] as bool? ?? false); + case 'scrollChanged': + onScrollChanged?.call(a['x'] as int? ?? 0, a['y'] as int? ?? 0); + } + } +} + +/// A transparent placeholder over the web region. +/// +/// It paints nothing (the engine's compositor surface shows through beneath the +/// Flutter scene), reports its on-screen rect via [OscWebViewController.setViewport] +/// after layout, and forwards pointer/scroll input transformed to webview-local +/// coordinates. Keyboard text should be forwarded by the host via +/// [OscWebViewController.dispatchKey] while this view holds focus. +class OscWebView extends StatefulWidget { + const OscWebView({super.key, required this.controller}); + + final OscWebViewController controller; + + @override + State createState() => _OscWebViewState(); +} + +class _OscWebViewState extends State { + final GlobalKey _key = GlobalKey(); + Rect? _lastViewport; + + RenderBox? get _box => + _key.currentContext?.findRenderObject() as RenderBox?; + + Offset _toLocal(Offset global) => _box?.globalToLocal(global) ?? global; + + void _reportViewport() { + final RenderBox? box = _box; + if (box == null || !box.hasSize) return; + final Offset origin = box.localToGlobal(Offset.zero); + final Rect rect = origin & box.size; + if (rect == _lastViewport) return; // only on change + _lastViewport = rect; + widget.controller.setViewport( + x: origin.dx.round(), + y: origin.dy.round(), + w: box.size.width.round(), + h: box.size.height.round(), + ); + } + + @override + Widget build(BuildContext context) { + WidgetsBinding.instance + .addPostFrameCallback((_) => _reportViewport()); + return Listener( + key: _key, + behavior: HitTestBehavior.opaque, + onPointerDown: (PointerDownEvent e) { + final Offset p = _toLocal(e.position); + widget.controller.dispatchInput( + type: OscInput.pointerDown, x: p.dx, y: p.dy, button: e.buttons); + }, + onPointerMove: (PointerMoveEvent e) { + final Offset p = _toLocal(e.position); + widget.controller.dispatchInput( + type: OscInput.pointerMove, x: p.dx, y: p.dy, button: e.buttons); + }, + onPointerUp: (PointerUpEvent e) { + final Offset p = _toLocal(e.position); + widget.controller + .dispatchInput(type: OscInput.pointerUp, x: p.dx, y: p.dy); + }, + onPointerHover: (PointerHoverEvent e) { + final Offset p = _toLocal(e.position); + widget.controller + .dispatchInput(type: OscInput.pointerHover, x: p.dx, y: p.dy); + }, + onPointerSignal: (PointerSignalEvent e) { + if (e is PointerScrollEvent) { + final Offset p = _toLocal(e.position); + widget.controller.dispatchScroll( + x: p.dx, y: p.dy, dx: e.scrollDelta.dx, dy: e.scrollDelta.dy); + } + }, + // Transparent: the web surface shows through beneath the Flutter scene. + child: const SizedBox.expand(), + ); + } +} diff --git a/packages/oscortex_webview/pubspec.yaml b/packages/oscortex_webview/pubspec.yaml new file mode 100644 index 0000000..036b834 --- /dev/null +++ b/packages/oscortex_webview/pubspec.yaml @@ -0,0 +1,21 @@ +name: oscortex_webview +description: > + OSCortex native webview for userspace Flutter apps. Drives a userspace web-engine + service over the `oscortex/webview` platform channel; the engine renders into its + own OSCortex compositor surface stacked beneath the app's chrome, and this package + provides the controller + a placeholder widget that reserves the region and + forwards input. Engine-agnostic (Servo today; the contract is fixed). +version: 0.1.0 +publish_to: none + +environment: + sdk: ^3.11.0 + flutter: '>=3.29.0' + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter diff --git a/packages/oscortex_webview/test/oscortex_webview_test.dart b/packages/oscortex_webview/test/oscortex_webview_test.dart new file mode 100644 index 0000000..8cbe8a4 --- /dev/null +++ b/packages/oscortex_webview/test/oscortex_webview_test.dart @@ -0,0 +1,146 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:oscortex_webview/oscortex_webview.dart'; + +/// Verifies the `oscortex/webview` MethodChannel contract from the app side: +/// methods send the right name/args, replies decode, engine→app events fire the +/// matching callbacks, and events route to the correct instance by viewId. The +/// engine is mocked, so these run without a device. +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + const String name = 'oscortex/webview'; + const MethodChannel channel = MethodChannel(name); + const StandardMethodCodec codec = StandardMethodCodec(); + final List calls = []; + + TestDefaultBinaryMessenger m() => + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger; + + // Simulate an engine→app event by delivering an encoded MethodCall. + Future emit(String method, Map args) async { + final ByteData data = codec.encodeMethodCall(MethodCall(method, args)); + await m().handlePlatformMessage(name, data, (_) {}); + } + + setUp(() { + calls.clear(); + m().setMockMethodCallHandler(channel, (MethodCall c) async { + calls.add(c); + switch (c.method) { + case 'create': + return {'surfaceId': 42}; + case 'canGoBack': + return true; + case 'canGoForward': + return false; + case 'currentUrl': + return 'https://example.com/'; + case 'getTitle': + return 'Example'; + case 'evalJs': + return 7; + default: + return null; + } + }); + }); + + tearDown(() => m().setMockMethodCallHandler(channel, null)); + + test('create sends size/dpr/url and returns the surfaceId', () async { + final OscWebViewController c = OscWebViewController(1); + final int sid = + await c.create(width: 800, height: 600, dpr: 2.0, initialUrl: 'https://a.test/'); + expect(sid, 42); + final MethodCall call = calls.single; + expect(call.method, 'create'); + expect(call.arguments['viewId'], 1); + expect(call.arguments['width'], 800); + expect(call.arguments['height'], 600); + expect(call.arguments['dpr'], 2.0); + expect(call.arguments['initialUrl'], 'https://a.test/'); + c.dispose(); + }); + + test('navigation methods send method + viewId', () async { + final OscWebViewController c = OscWebViewController(1); + await c.loadUrl('https://x.test/'); + await c.goBack(); + await c.goForward(); + await c.reload(); + expect(calls.map((MethodCall e) => e.method).toList(), + ['loadUrl', 'goBack', 'goForward', 'reload']); + expect(calls.first.arguments['url'], 'https://x.test/'); + expect(calls.every((MethodCall e) => e.arguments['viewId'] == 1), isTrue); + c.dispose(); + }); + + test('query methods round-trip their replies', () async { + final OscWebViewController c = OscWebViewController(1); + expect(await c.canGoBack(), true); + expect(await c.canGoForward(), false); + expect(await c.currentUrl(), 'https://example.com/'); + expect(await c.getTitle(), 'Example'); + expect(await c.evalJs('1+6'), 7); + c.dispose(); + }); + + test('dispatchInput forwards type/coords/button', () async { + final OscWebViewController c = OscWebViewController(1); + await c.dispatchInput( + type: OscInput.pointerDown, x: 12.5, y: 34.0, button: 1); + final MethodCall call = calls.single; + expect(call.method, 'dispatchInput'); + expect(call.arguments['type'], OscInput.pointerDown); + expect(call.arguments['x'], 12.5); + expect(call.arguments['y'], 34.0); + expect(call.arguments['button'], 1); + c.dispose(); + }); + + test('engine events fire the matching callbacks', () async { + final OscWebViewController c = OscWebViewController(1); + String? url; + String? title; + int? progress; + bool? back; + bool? fwd; + c.onUrlChanged = (String u) => url = u; + c.onTitleChanged = (String t) => title = t; + c.onProgress = (int p) => progress = p; + c.onNavState = (bool b, bool f) { + back = b; + fwd = f; + }; + + await emit('urlChanged', {'viewId': 1, 'url': 'https://e.test/'}); + await emit('titleChanged', {'viewId': 1, 'title': 'E'}); + await emit('loadProgress', {'viewId': 1, 'progress': 55}); + await emit('navState', + {'viewId': 1, 'canGoBack': true, 'canGoForward': false}); + + expect(url, 'https://e.test/'); + expect(title, 'E'); + expect(progress, 55); + expect(back, true); + expect(fwd, false); + c.dispose(); + }); + + test('events route to the correct instance by viewId', () async { + final OscWebViewController c1 = OscWebViewController(1); + final OscWebViewController c2 = OscWebViewController(2); + String? u1; + String? u2; + c1.onUrlChanged = (String u) => u1 = u; + c2.onUrlChanged = (String u) => u2 = u; + + await emit('urlChanged', + {'viewId': 2, 'url': 'https://two.test/'}); + + expect(u2, 'https://two.test/'); + expect(u1, isNull); // instance 1 untouched + c1.dispose(); + c2.dispose(); + }); +} diff --git a/rules.md b/rules.md new file mode 100644 index 0000000..54ff47f --- /dev/null +++ b/rules.md @@ -0,0 +1,68 @@ +# OSCortex Engineering Rules — MANDATORY for every agent and contributor + +These rules exist because stubbed/incomplete work was shipped or reported as if it +were finished. That wastes the maintainer's time and hides risk. **Read this before +starting any task. Re-read it before reporting a task complete.** No exceptions. + +--- + +## 1. NEVER present a stub as done. Ever. +A stub, mock, placeholder, hardcoded value, fake response, or "happy-path only" +implementation is **NOT** a finished feature. + +- If you write one, you **MUST** label it in the code on the line above it: + `// STUB(): — real impl needs . Tracked: ` +- You **MUST** report it as a stub in your status, using the exact word "stub". +- You may **NEVER** say/write "done", "working", "implemented", "complete", or + "✅" for anything that contains a stub on its critical path. + +## 2. "Done" has one definition: verified working end-to-end. +- Compiles ≠ done. Type-checks ≠ done. Renders a UI ≠ done. Tests pass ≠ done if the + tests test the stub. +- "Done" means: you ran the **real artifact** (the actual ISO/binary/app the user + would run — not a proxy, not a unit test of one layer) and **observed the real + behavior produce the real result**, with proof (screenshot, log, output) saved. +- If you cannot verify it end-to-end, it is **NOT done** — it is "implemented, + unverified". Say that. + +## 3. Report status in one of exactly four states. No fuzzy words. +For every feature/task you touch, classify it as exactly one of: +- **DONE** — implemented AND verified end-to-end on the real artifact (proof exists). +- **UNVERIFIED** — implemented, compiles, but not yet run/proven on the real artifact. +- **SCAFFOLD/STUB** — plumbing or UI exists; core behavior is faked/missing. +- **NOT STARTED**. + +Never blur these. "We have a webview" when the engine is a stub = lying by omission. + +## 4. Finish the task, or name the gap precisely. +You may not silently narrow scope. If you can't finish: +- State **exactly** what is missing, **why**, and a **realistic effort estimate**. +- Leave a scoped `TODO(): — — est ` in the code. +- Do not downgrade a request ("build the browser") into a lesser thing ("build a + browser-shaped placeholder") and call it the same task. + +## 5. Verify on the REAL thing before any "it works" claim. +- Don't test a debug/instrumented build and claim the release build works — build and + run the **actual release artifact**. +- Don't infer from one layer. The user runs the whole stack; verify the whole stack. +- "Published == verified": nothing ships/goes to cloud unless its exact bytes were run. + +## 6. When unsure whether something is complete, assume it is NOT, and check. +Default to skepticism about your own prior work. Open the file. Run it. Look. + +## 7. Stubs are allowed ONLY as explicit, tracked stepping stones. +A stub is legitimate to *unblock a pipeline* — but only when (a) labeled per Rule 1, +(b) reported per Rule 3, and (c) the real implementation is tracked as the next step. +A stub is never an acceptable final deliverable for a requested feature. + +## 8. Maintain an honest feature ledger. +Keep `docs/FEATURE_STATUS.md` current: every user-facing feature, its state (one of +the four in Rule 3), and the gap if not DONE. Update it whenever you touch a feature. +This is how we stop "I'm pretty sure that's stubbed across the OS" from being true. + +--- + +### Why this is non-negotiable +The maintainer works against time and trusts status reports to plan. A false "done" +breaks that trust and costs more time than the work saved. Honesty about an unfinished +thing is always cheaper than a hidden stub. Slower-but-true beats fast-but-fake. diff --git a/scripts/build-iso.sh b/scripts/build-iso.sh index 3fc1dd1..9e2aec0 100755 --- a/scripts/build-iso.sh +++ b/scripts/build-iso.sh @@ -81,12 +81,28 @@ if [ -n "${SKIP_CORE_APPS:-}" ]; then # JIT off kernel_blob.bin — arch-independent — so the staged bundles still render # their launcher tiles). Set SKIP_CORE_APPS=1 to take this path. echo "[0.35/5] SKIP_CORE_APPS set — reusing staged app assets (Docker/oscx-engine not required)" - for a in "Canvas.app/flutter_assets/kernel_blob.bin" "Files.app/flutter_assets/kernel_blob.bin" "Web Link.app/flutter_assets/kernel_blob.bin"; do - if [ ! -f "$ROOT/initramfs/Applications/$a" ]; then - echo "ERROR: SKIP_CORE_APPS set but staged app asset missing: initramfs/Applications/$a" >&2 - exit 1 - fi - done + if [ -n "${X86_AOT:-}" ]; then + # AOT path uses prebuilt libapp.so snapshots, not JIT kernel_blob — require those. + for a in "Files.app" "Canvas.app" "Web Link.app"; do + src="" + case "$a" in + "Files.app") src="oscortex_files" ;; + "Canvas.app") src="oscortex_canvas" ;; + "Web Link.app") src="oscortex_web_link" ;; + esac + if [ ! -f "$ROOT/apps/$src/build/oscortex/libapp.so" ]; then + echo "ERROR: X86_AOT set but app AOT snapshot missing: apps/$src/build/oscortex/libapp.so" >&2 + exit 1 + fi + done + else + for a in "Canvas.app/flutter_assets/kernel_blob.bin" "Files.app/flutter_assets/kernel_blob.bin" "Web Link.app/flutter_assets/kernel_blob.bin"; do + if [ ! -f "$ROOT/initramfs/Applications/$a" ]; then + echo "ERROR: SKIP_CORE_APPS set but staged app asset missing: initramfs/Applications/$a" >&2 + exit 1 + fi + done + fi else echo "[0.35/5] Building core system apps into /Applications..." "$ROOT/tools/build-flutter-osx.sh" \ @@ -110,17 +126,21 @@ if [ -d "$APP_ASSETS_DIR" ]; then echo "[0.4/5] Syncing shell Flutter assets into initramfs..." mkdir -p "$ROOT/initramfs/system/flutter/flutter_assets" - for f in kernel_blob.bin; do - if [ ! -f "$APP_ASSETS_DIR/$f" ]; then - echo "ERROR: required app asset missing: $APP_ASSETS_DIR/$f" >&2 - exit 1 - fi - cp "$APP_ASSETS_DIR/$f" "$ROOT/initramfs/system/flutter/flutter_assets/$f" - done - python3 "$ROOT/tools/flutter-engine/engine_patch.py" --kernel-blob "$ROOT/initramfs/system/flutter/flutter_assets/kernel_blob.bin" - python3 "$ROOT/tools/flutter-engine/engine_patch.py" --kernel-blob "$ROOT/initramfs/Applications/Canvas.app/flutter_assets/kernel_blob.bin" - python3 "$ROOT/tools/flutter-engine/engine_patch.py" --kernel-blob "$ROOT/initramfs/Applications/Files.app/flutter_assets/kernel_blob.bin" - python3 "$ROOT/tools/flutter-engine/engine_patch.py" --kernel-blob "$ROOT/initramfs/Applications/Web Link.app/flutter_assets/kernel_blob.bin" + # JIT path needs kernel_blob.bin (the Dart program for the JIT engine); the + # X86_AOT path carries the program in libapp.so and skips this entirely. + if [ -z "${X86_AOT:-}" ]; then + for f in kernel_blob.bin; do + if [ ! -f "$APP_ASSETS_DIR/$f" ]; then + echo "ERROR: required app asset missing: $APP_ASSETS_DIR/$f" >&2 + exit 1 + fi + cp "$APP_ASSETS_DIR/$f" "$ROOT/initramfs/system/flutter/flutter_assets/$f" + done + python3 "$ROOT/tools/flutter-engine/engine_patch.py" --kernel-blob "$ROOT/initramfs/system/flutter/flutter_assets/kernel_blob.bin" + python3 "$ROOT/tools/flutter-engine/engine_patch.py" --kernel-blob "$ROOT/initramfs/Applications/Canvas.app/flutter_assets/kernel_blob.bin" + python3 "$ROOT/tools/flutter-engine/engine_patch.py" --kernel-blob "$ROOT/initramfs/Applications/Files.app/flutter_assets/kernel_blob.bin" + python3 "$ROOT/tools/flutter-engine/engine_patch.py" --kernel-blob "$ROOT/initramfs/Applications/Web Link.app/flutter_assets/kernel_blob.bin" + fi # Each launched app is its own Flutter host and hits the same bare-metal font # issue as the shell: no system font provider + no default/Roboto family -> @@ -230,11 +250,17 @@ REQUIRED_FILES=( "$ROOT/initramfs/Applications/Canvas.app/Canvas.osx" "$ROOT/initramfs/Applications/Files.app/Files.osx" "$ROOT/initramfs/Applications/Web Link.app/Web Link.osx" - "$ROOT/initramfs/system/flutter/flutter_assets/kernel_blob.bin" - "$ROOT/initramfs/Applications/Canvas.app/flutter_assets/kernel_blob.bin" - "$ROOT/initramfs/Applications/Files.app/flutter_assets/kernel_blob.bin" - "$ROOT/initramfs/Applications/Web Link.app/flutter_assets/kernel_blob.bin" ) +# JIT path requires kernel_blob; the X86_AOT path stages + validates its libapp.so +# snapshots in the dedicated block below, so it skips the kernel_blob requirement. +if [ -z "${X86_AOT:-}" ]; then + REQUIRED_FILES+=( + "$ROOT/initramfs/system/flutter/flutter_assets/kernel_blob.bin" + "$ROOT/initramfs/Applications/Canvas.app/flutter_assets/kernel_blob.bin" + "$ROOT/initramfs/Applications/Files.app/flutter_assets/kernel_blob.bin" + "$ROOT/initramfs/Applications/Web Link.app/flutter_assets/kernel_blob.bin" + ) +fi for req in "${REQUIRED_FILES[@]}"; do if [ ! -f "$req" ]; then echo "ERROR: required Flutter artifact missing: $req" >&2 @@ -242,21 +268,55 @@ for req in "${REQUIRED_FILES[@]}"; do fi done -# x86_64 runs the shell via the JIT engine off kernel_blob.bin (the staged engine -# MUST be the JIT/debug engine that contains the Dart kernel compiler, NOT the AOT -# 'product' engine — the AOT path needs a matching 'product' gen_snapshot that is -# not available, so it leaves the shell unable to load Dart → blank screen). Remove -# any AOT snapshot so the embedder uses the arch-independent JIT kernel_blob. -rm -f "$ROOT/initramfs/system/flutter/libapp.so" \ - "$ROOT/initramfs/system/flutter/app.aot" +# x86_64 AOT path (X86_AOT=1): the patched release x64 engine is AOT-only (reports +# FlutterEngineRunsAOTCompiledDartCode=true), so the shell/apps must load their AOT +# libapp.so snapshots — NOT kernel_blob (the release engine has no Dart kernel +# compiler). The matching x86-64 AOT snapshots are prebuilt under apps/*/build/ +# oscortex/libapp.so (version-matched to the engine). Stage them like the arm64 +# flow. Without X86_AOT, keep the legacy JIT path (strip AOT snapshots, use +# kernel_blob) for a JIT/debug engine. +if [ -n "${X86_AOT:-}" ]; then + echo "[1.5/5] X86_AOT: staging x86-64 AOT snapshots (shell + apps)..." + SHELL_AOT="$ROOT/apps/oscortex_app/build/oscortex/libapp.so" + [ -f "$SHELL_AOT" ] || { echo "ERROR: X86_AOT but shell snapshot missing: $SHELL_AOT" >&2; exit 1; } + cp "$SHELL_AOT" "$ROOT/initramfs/system/flutter/libapp.so" + # Map each bundled app to its prebuilt x86-64 AOT snapshot. + stage_app_aot() { # + local src="$ROOT/apps/$1/build/oscortex/libapp.so" + local dst="$ROOT/initramfs/Applications/$2/libapp.so" + if [ -f "$src" ]; then cp "$src" "$dst"; echo " staged $2/libapp.so"; \ + else echo " WARN: missing app AOT snapshot $src" >&2; fi + } + stage_app_aot oscortex_files "Files.app" + stage_app_aot oscortex_canvas "Canvas.app" + stage_app_aot oscortex_web_link "Web Link.app" + # AOT carries the program in libapp.so — drop the JIT kernel_blob so the + # embedder takes the AOT path (mirrors the arm64 build). + rm -f "$ROOT/initramfs/system/flutter/flutter_assets/kernel_blob.bin" \ + "$ROOT/initramfs/Applications/Files.app/flutter_assets/kernel_blob.bin" \ + "$ROOT/initramfs/Applications/Canvas.app/flutter_assets/kernel_blob.bin" \ + "$ROOT/initramfs/Applications/Web Link.app/flutter_assets/kernel_blob.bin" +else + # Legacy JIT path: strip AOT snapshots so the embedder uses kernel_blob. + rm -f "$ROOT/initramfs/system/flutter/libapp.so" \ + "$ROOT/initramfs/system/flutter/app.aot" +fi echo "[1/5] Building kernel ELF..." touch "$ROOT/kernel/src/fs/initramfs.rs" cd "$ROOT" +# Extra kernel cargo features (additive to the default arch-x86_64), e.g. +# KERNEL_FEATURES=input-hud for the on-screen input debug HUD. See dev-tools/README.md. +KERNEL_FEATURE_ARGS=() +if [ -n "${KERNEL_FEATURES:-}" ]; then + echo "[1/5] + kernel features: $KERNEL_FEATURES" + KERNEL_FEATURE_ARGS=(--features "$KERNEL_FEATURES") +fi cargo +nightly build \ --release \ --package oscortex-kernel \ --target x86_64-unknown-none \ + ${KERNEL_FEATURE_ARGS[@]+"${KERNEL_FEATURE_ARGS[@]}"} \ -Z build-std=core,compiler_builtins,alloc \ -Z build-std-features=compiler-builtins-mem diff --git a/tools/flutter-embedder/src/main.rs b/tools/flutter-embedder/src/main.rs index 54c16c4..71037e0 100644 --- a/tools/flutter-embedder/src/main.rs +++ b/tools/flutter-embedder/src/main.rs @@ -890,6 +890,20 @@ fn dispatch_platform_channel(channel: &[u8], msg: &FlutterPlatformMessage) { handle_shell_platform_message(msg); } + // ── App networking (BasicMessageChannel, binary codec) ───── + // Compact opcode framing → kernel TCP syscalls. See handle_net_channel. + b"oscortex/net" => { + handle_net_channel(msg); + } + + // ── Native webview (MethodChannel, StandardMethodCodec) ────────────── + // Manages a compositor surface for web content + events. The renderer + // behind it is a stub today; a Servo service replaces it. See + // handle_webview_channel + docs/browser-architecture.md. + b"oscortex/webview" => { + handle_webview_channel(msg); + } + // ── Text input (JSONMethodCodec) — THE critical channel ────────── b"flutter/textinput" => { handle_textinput_channel(msg); @@ -915,16 +929,28 @@ fn dispatch_platform_channel(channel: &[u8], msg: &FlutterPlatformMessage) { respond_platform_message(msg, &[0x00u8]); } - b"flutter/navigation" - | b"flutter/system" + // StandardMethodCodec channels: the framework decodes the reply with + // StandardMethodCodec, so a success-null reply MUST be the binary + // envelope [0x00, 0x00] — NOT JSON `[null]`. Replying JSON here makes the + // framework throw `FormatException: Message corrupted` in + // StandardMethodCodec.decodeEnvelope. flutter/restoration is the loud one: + // RestorationManager probes it with StandardMethodCodec at startup on + // EVERY engine, so a JSON reply throws once per engine (shell + each app). + b"flutter/restoration" + | b"flutter/platform_views" | b"flutter/spellcheck" | b"flutter/processtext" | b"flutter/menu" + | b"flutter/scribe" => { + respond_platform_message(msg, METHOD_SUCCESS_NULL); + } + + // JSONMethodCodec / BasicMessageChannel (JSON/String) channels: a JSON + // `[null]` envelope is the correct graceful ack. + b"flutter/navigation" + | b"flutter/system" | b"flutter/contextmenu" - | b"flutter/scribe" - | b"flutter/restoration" | b"flutter/keyevent" - | b"flutter/platform_views" | b"flutter/isolate" | b"flutter/lifecycle" => { respond_platform_message(msg, JSON_SUCCESS_NULL); @@ -943,6 +969,398 @@ fn dispatch_platform_channel(channel: &[u8], msg: &FlutterPlatformMessage) { } } +/// Max bytes returned by a single `recv` (apps call repeatedly for more). +const NET_RECV_CAP: usize = 8192; + +/// App networking channel — `oscortex/net`, a `BasicMessageChannel` +/// (binary codec) so TCP byte streams pass through unmangled. The Dart message +/// is a compact opcode frame; the reply is raw little-endian bytes: +/// +/// connect [0x01, ip(4 BE), port(2 BE)] → i32 fd +/// status [0x02, fd(4 LE)] → i32 (1 established / 0 connecting / <0 err) +/// send [0x03, fd(4 LE), data…] → i32 bytes written (-11 = EAGAIN) +/// recv [0x04, fd(4 LE), max(4 LE)] → i32 n, then n data bytes (-11 = no data yet) +/// close [0x05, fd(4 LE)] → i32 (0 / <0 err) +/// +/// All ints are little-endian. Errors are negative errnos straight from the +/// kernel TCP syscalls (which this just marshals). +fn handle_net_channel(msg: &FlutterPlatformMessage) { + let payload = if msg.message != 0 && msg.message_size != 0 { + unsafe { core::slice::from_raw_parts(msg.message as *const u8, msg.message_size) } + } else { + net_reply_i32(msg, -22); // EINVAL + return; + }; + + fn rd_u32_le(b: &[u8], off: usize) -> u32 { + if off + 4 <= b.len() { + u32::from_le_bytes([b[off], b[off + 1], b[off + 2], b[off + 3]]) + } else { + 0 + } + } + + match payload[0] { + 0x01 => { + // connect: ip is big-endian (network order), port big-endian. + if payload.len() < 7 { + net_reply_i32(msg, -22); + return; + } + let ip = u32::from_be_bytes([payload[1], payload[2], payload[3], payload[4]]); + let port = u16::from_be_bytes([payload[5], payload[6]]); + net_reply_i32(msg, sys::tcp_connect(ip, port) as i32); + } + 0x02 => net_reply_i32(msg, sys::tcp_status(rd_u32_le(payload, 1)) as i32), + 0x03 => { + let fd = rd_u32_le(payload, 1); + let data: &[u8] = if payload.len() > 5 { &payload[5..] } else { &[] }; + net_reply_i32(msg, sys::tcp_write(fd, data) as i32); + } + 0x04 => { + let fd = rd_u32_le(payload, 1); + let max = (rd_u32_le(payload, 5) as usize).min(NET_RECV_CAP); + // Single buffer: [i32 n | data]. Read into [4..], then prefix n. + let mut out = [0u8; 4 + NET_RECV_CAP]; + let n = sys::tcp_read(fd, &mut out[4..4 + max]) as i32; + let body = if n > 0 { (n as usize).min(max) } else { 0 }; + out[..4].copy_from_slice(&n.to_le_bytes()); + respond_platform_message(msg, &out[..4 + body]); + } + 0x05 => net_reply_i32(msg, sys::tcp_close(rd_u32_le(payload, 1)) as i32), + 0x06 => { + // resolve: [0x06, hostname bytes] → reply u32 LE (BE-order IPv4), + // or 0 on failure. (NOT net_reply_i32: a valid high-bit IP like + // 200.x.x.x would look negative as i32; reply the raw u32 instead.) + let name: &[u8] = if payload.len() > 1 { &payload[1..] } else { &[] }; + let r = sys::dns_resolve(name); + let ip: u32 = if r >= 0 { r as u32 } else { 0 }; + respond_platform_message(msg, &ip.to_le_bytes()); + } + _ => net_reply_i32(msg, -22), + } +} + +/// Reply on `oscortex/net` with a little-endian i32 result/status. +fn net_reply_i32(msg: &FlutterPlatformMessage, v: i32) { + respond_platform_message(msg, &v.to_le_bytes()); +} + +// ════════════════════════════════════════════════════════════════════════════ +// Native webview — `oscortex/webview` (StandardMethodCodec). +// +// The web engine renders into its OWN compositor surface, z-stacked above this +// app's Flutter surface and clipped to the viewport the Web Link app reports, so +// it shows in the web region beneath the chrome. The renderer here is a STUB +// (fills the surface a solid color + drives navigation events) that proves the +// whole pipeline — app → channel → surface → composite → events — end to end +// without a real engine; a Servo service replaces the stub behind this contract. +// See docs/browser-architecture.md. + +/// One native webview instance (single-view for now). +struct WebviewState { + view_id: i32, + surface_id: u32, + width: u32, + height: u32, + buf_va: u64, // mmap'd scratch RGBA buffer + buf_len: usize, +} +static mut WEBVIEW: Option = None; +/// Web surface stacks above the app's Flutter surface (clipped to the web rect). +const WEBVIEW_Z: i32 = 1000; +static WEBVIEW_CH: &[u8] = b"oscortex/webview\0"; + +// ── StandardMethodCodec decode (generalizes std_find_kind) ─────────────────── + +/// The method name of a StandardMethodCodec call (value 0 = a string). +fn std_method_name(buf: &[u8]) -> Option<&[u8]> { + if *buf.first()? != 7 { + return None; + } + let (n, j) = std_read_size(buf, 1)?; + buf.get(j..j + n) +} + +/// Offset of the value for `key` in the arguments map (value 1, a map). +fn std_arg_value_off(buf: &[u8], key: &[u8]) -> Option { + let i = std_skip_value(buf, 0)?; // past the method name + if *buf.get(i)? != 13 { + return None; // arguments must be a map + } + let (n, mut j) = std_read_size(buf, i + 1)?; + for _ in 0..n { + let key_start = j; + let after_key = std_skip_value(buf, j)?; + let after_val = std_skip_value(buf, after_key)?; + if *buf.get(key_start)? == 7 { + let (klen, ks) = std_read_size(buf, key_start + 1)?; + if buf.get(ks..ks + klen)? == key { + return Some(after_key); + } + } + j = after_val; + } + None +} + +fn std_arg_str<'a>(buf: &'a [u8], key: &[u8]) -> Option<&'a [u8]> { + let v = std_arg_value_off(buf, key)?; + if *buf.get(v)? != 7 { + return None; + } + let (n, s) = std_read_size(buf, v + 1)?; + buf.get(s..s + n) +} + +fn std_arg_int(buf: &[u8], key: &[u8]) -> Option { + let v = std_arg_value_off(buf, key)?; + match *buf.get(v)? { + 3 => { + let b = buf.get(v + 1..v + 5)?; + Some(i32::from_le_bytes([b[0], b[1], b[2], b[3]]) as i64) + } + 4 => { + let b = buf.get(v + 1..v + 9)?; + Some(i64::from_le_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]])) + } + _ => None, + } +} + +// ── StandardMethodCodec encode into a fixed buffer (no alloc) ──────────────── + +struct StdEnc { + buf: [u8; 512], + len: usize, +} +impl StdEnc { + fn new() -> Self { + Self { buf: [0; 512], len: 0 } + } + fn raw(&mut self, x: u8) { + if self.len < self.buf.len() { + self.buf[self.len] = x; + self.len += 1; + } + } + fn raw_bytes(&mut self, s: &[u8]) { + for &x in s { + self.raw(x); + } + } + fn size(&mut self, n: usize) { + if n < 254 { + self.raw(n as u8); + } else if n <= 0xFFFF { + self.raw(254); + self.raw(n as u8); + self.raw((n >> 8) as u8); + } else { + self.raw(255); + self.raw(n as u8); + self.raw((n >> 8) as u8); + self.raw((n >> 16) as u8); + self.raw((n >> 24) as u8); + } + } + fn val_str(&mut self, s: &[u8]) { + self.raw(7); + self.size(s.len()); + self.raw_bytes(s); + } + fn val_i32(&mut self, v: i32) { + self.raw(3); + self.raw_bytes(&v.to_le_bytes()); + } + fn val_bool(&mut self, v: bool) { + self.raw(if v { 1 } else { 2 }); + } + fn map(&mut self, n: usize) { + self.raw(13); + self.size(n); + } + fn slice(&self) -> &[u8] { + &self.buf[..self.len] + } +} + +// ── Event emitters (engine → app: StandardMethodCodec method calls) ────────── + +fn webview_send(payload: &[u8]) { + let send_fn_va = SEND_PLATFORM_MESSAGE_FN.load(Ordering::SeqCst); + let engine = ENGINE.load(Ordering::SeqCst); + if send_fn_va == 0 || engine == 0 { + return; + } + let msg = FlutterPlatformMessage { + struct_size: core::mem::size_of::(), + channel: WEBVIEW_CH.as_ptr() as u64, + message: payload.as_ptr() as u64, + message_size: payload.len(), + response_handle: 0, + }; + let send: SendPlatformMessageFn = unsafe { core::mem::transmute(send_fn_va) }; + let _ = unsafe { send(engine, &msg as *const _) }; +} + +/// Emit a `{viewId, url}` event (urlChanged / loadStarted / loadFinished). +fn webview_emit_url(view_id: i32, method: &[u8], url: &[u8]) { + let mut e = StdEnc::new(); + e.val_str(method); + e.map(2); + e.val_str(b"viewId"); + e.val_i32(view_id); + e.val_str(b"url"); + e.val_str(url); + webview_send(e.slice()); +} + +fn webview_emit_progress(view_id: i32, progress: i32) { + let mut e = StdEnc::new(); + e.val_str(b"loadProgress"); + e.map(2); + e.val_str(b"viewId"); + e.val_i32(view_id); + e.val_str(b"progress"); + e.val_i32(progress); + webview_send(e.slice()); +} + +fn webview_emit_nav(view_id: i32, back: bool, fwd: bool) { + let mut e = StdEnc::new(); + e.val_str(b"navState"); + e.map(3); + e.val_str(b"viewId"); + e.val_i32(view_id); + e.val_str(b"canGoBack"); + e.val_bool(back); + e.val_str(b"canGoForward"); + e.val_bool(fwd); + webview_send(e.slice()); +} + +/// Stub render: fill the web surface with a solid RGBA color + submit it. +unsafe fn webview_fill(st: &mut WebviewState, rgba: u32) { + let needed = (st.width as usize) * (st.height as usize) * 4; + if st.buf_va == 0 || st.buf_len < needed { + st.buf_va = sys::mmap_anon(needed); + st.buf_len = needed; + } + if st.buf_va == 0 { + return; + } + let px = unsafe { + core::slice::from_raw_parts_mut(st.buf_va as *mut u32, (st.width * st.height) as usize) + }; + for p in px.iter_mut() { + *p = rgba; + } + let bytes = + unsafe { core::slice::from_raw_parts(st.buf_va as *const u8, needed) }; + let _ = sys::gpu_submit_strided(st.surface_id, bytes, (st.width as usize) * 4); +} + +fn handle_webview_channel(msg: &FlutterPlatformMessage) { + let payload = if msg.message != 0 && msg.message_size != 0 { + unsafe { core::slice::from_raw_parts(msg.message as *const u8, msg.message_size) } + } else { + respond_platform_message(msg, METHOD_SUCCESS_NULL); + return; + }; + let method = std_method_name(payload).unwrap_or(b""); + let view_id = std_arg_int(payload, b"viewId").unwrap_or(0) as i32; + + match method { + b"create" => { + let w = std_arg_int(payload, b"width").unwrap_or(1).max(1) as u32; + let h = std_arg_int(payload, b"height").unwrap_or(1).max(1) as u32; + let sid = sys::surface_create(w, h); + let surface = if sid >= 0 { + let sid = sid as u32; + let mut st = WebviewState { + view_id, + surface_id: sid, + width: w, + height: h, + buf_va: 0, + buf_len: 0, + }; + unsafe { + webview_fill(&mut st, 0xFF2A2A2A); // dark gray placeholder + *core::ptr::addr_of_mut!(WEBVIEW) = Some(st); + } + sid as i32 + } else { + -1 + }; + let mut e = StdEnc::new(); + e.raw(0x00); // success envelope + e.map(1); + e.val_str(b"surfaceId"); + e.val_i32(surface); + respond_platform_message(msg, e.slice()); + if surface >= 0 { + webview_emit_nav(view_id, false, false); + } + } + b"setViewport" => { + let x = std_arg_int(payload, b"x").unwrap_or(0) as i32; + let y = std_arg_int(payload, b"y").unwrap_or(0) as i32; + let w = std_arg_int(payload, b"w").unwrap_or(0).max(0) as u32; + let h = std_arg_int(payload, b"h").unwrap_or(0).max(0) as u32; + unsafe { + if let Some(st) = (*core::ptr::addr_of!(WEBVIEW)).as_ref() { + let sid = st.surface_id; + let _ = sys::surface_geometry_set(sid, x, y, w, h); + let _ = sys::surface_clip_set(sid, x, y, w, h); + let _ = sys::surface_z_set(sid, WEBVIEW_Z); + } + } + respond_platform_message(msg, METHOD_SUCCESS_NULL); + } + b"loadUrl" => { + let url = std_arg_str(payload, b"url").unwrap_or(b""); + webview_emit_url(view_id, b"loadStarted", url); + webview_emit_url(view_id, b"urlChanged", url); + unsafe { + if let Some(st) = (*core::ptr::addr_of_mut!(WEBVIEW)).as_mut() { + webview_fill(st, 0xFF202830); // tint to show the nav took effect + } + } + webview_emit_progress(view_id, 100); + webview_emit_url(view_id, b"loadFinished", url); + webview_emit_nav(view_id, false, false); + respond_platform_message(msg, METHOD_SUCCESS_NULL); + } + b"destroy" => { + unsafe { + if let Some(st) = (*core::ptr::addr_of!(WEBVIEW)).as_ref() { + let _ = sys::surface_destroy(st.surface_id); + } + *core::ptr::addr_of_mut!(WEBVIEW) = None; + } + respond_platform_message(msg, METHOD_SUCCESS_NULL); + } + b"canGoBack" | b"canGoForward" => { + let mut e = StdEnc::new(); + e.raw(0x00); + e.val_bool(false); + respond_platform_message(msg, e.slice()); + } + b"getTitle" => { + let mut e = StdEnc::new(); + e.raw(0x00); + e.val_str(b"OSCortex Web (stub)"); + respond_platform_message(msg, e.slice()); + } + // goBack / goForward / reload / stopLoading / resize / setVisible / + // dispatchInput / dispatchScroll / dispatchKey / evalJs / currentUrl: + // acknowledged (no-op in the stub). + _ => respond_platform_message(msg, METHOD_SUCCESS_NULL), + } +} + // ════════════════════════════════════════════════════════════════════════════ // Flutter framework platform channels (text input, cursor, clipboard, …) // @@ -1639,48 +2057,126 @@ fn textinput_feed_key(scancode: u32, pressed: bool) { // ── Mouse cursor channel ───────────────────────────────────────────────────── +// ── Minimal StandardMessageCodec reader (binary) ───────────────────────────── +// flutter/mousecursor uses StandardMethodCodec (a method call is +// writeValue(methodName:String) + writeValue(arguments:Map)), NOT JSON. Type +// bytes: 0=null 1=true 2=false 3=int32 4=int64 6=float64 7=string 8=uint8list +// 9=int32list 10=int64list 11=float64list 12=list 13=map. Strings/sizes use the +// readSize varint: <254 → 1 byte; 254 → u16; 255 → u32. + +/// Read a StandardMessageCodec size varint at `i`. Returns (size, next_index). +fn std_read_size(buf: &[u8], i: usize) -> Option<(usize, usize)> { + let b = *buf.get(i)? as usize; + if b < 254 { + Some((b, i + 1)) + } else if b == 254 { + Some(((*buf.get(i + 1)? as usize) | ((*buf.get(i + 2)? as usize) << 8), i + 3)) + } else { + let v = (*buf.get(i + 1)? as usize) + | ((*buf.get(i + 2)? as usize) << 8) + | ((*buf.get(i + 3)? as usize) << 16) + | ((*buf.get(i + 4)? as usize) << 24); + Some((v, i + 5)) + } +} + +/// Advance past one StandardMessageCodec value starting at `i`. Handles the +/// types that appear in framework method-call arguments (scalars, strings, +/// nested list/map). Returns the index after the value. +fn std_skip_value(buf: &[u8], i: usize) -> Option { + let t = *buf.get(i)?; + let i = i + 1; + match t { + 0 | 1 | 2 => Some(i), // null / true / false + 3 => Some(i + 4), // int32 + 4 => Some(i + 8), // int64 + 6 => Some(((i + 7) & !7) + 8), // float64, 8-byte aligned to buffer start + 7 | 8 => { // string / uint8list + let (n, j) = std_read_size(buf, i)?; + Some(j + n) + } + 12 | 13 => { // list / map + let (n, mut j) = std_read_size(buf, i)?; + let count = if t == 13 { n * 2 } else { n }; + for _ in 0..count { + j = std_skip_value(buf, j)?; + } + Some(j) + } + _ => None, + } +} + +/// Decode a StandardMethodCodec method call and return the string value of the +/// `kind` key in the arguments map, if present. +fn std_find_kind(buf: &[u8]) -> Option<&[u8]> { + // [0] = method name (string). Skip it. + if *buf.first()? != 7 { + return None; + } + let i = std_skip_value(buf, 0)?; + // arguments must be a Map (type 13). + if *buf.get(i)? != 13 { + return None; + } + let (n, mut j) = std_read_size(buf, i + 1)?; + for _ in 0..n { + // key + if *buf.get(j)? == 7 { + let (klen, kstart) = std_read_size(buf, j + 1)?; + let key = buf.get(kstart..kstart + klen)?; + let after_key = kstart + klen; + if key == b"kind" { + if *buf.get(after_key)? == 7 { + let (vlen, vstart) = std_read_size(buf, after_key + 1)?; + return buf.get(vstart..vstart + vlen); + } + return None; + } + j = std_skip_value(buf, after_key)?; + } else { + j = std_skip_value(buf, j)?; // skip non-string key + j = std_skip_value(buf, j)?; // skip its value + } + } + None +} + fn handle_mousecursor_channel(msg: &FlutterPlatformMessage) { let payload = if msg.message != 0 && msg.message_size != 0 { unsafe { core::slice::from_raw_parts(msg.message as *const u8, msg.message_size) } } else { - respond_platform_message(msg, JSON_SUCCESS_NULL); + // flutter/mousecursor is StandardMethodCodec: a success-null reply is the + // binary envelope [0x00, 0x00], NOT JSON `[null]` (which the framework's + // StandardMethodCodec.decodeEnvelope rejects as "Message corrupted"). + respond_platform_message(msg, METHOD_SUCCESS_NULL); return; }; - let mut method = [0u8; 64]; - if let Some((mlen, _)) = json_method_name(payload, &mut method) { - if &method[..mlen] == b"MouseCursor.activateSystemCursor" { - // args = { device, kind }. Map the Flutter cursor `kind` to a kernel - // cursor shape and apply it via SYS_CURSOR_SHAPE_SET so the compositor - // actually draws the matching pointer (hand on links, I-beam on text…). - if let Some(vi) = json_find_value(payload, b"kind") { - if payload.get(vi) == Some(&b'"') { - let mut kind = [0u8; 32]; - if let Some((klen, _)) = json_parse_string(payload, vi, &mut kind) { - let shape = cursor_kind_to_shape(&kind[..klen]); - // De-dupe: Flutter spams activateSystemCursor on every - // hover move. The kernel syscall redraws the cursor - // (invalidate → composite), so calling it per-event is a - // redraw STORM that freezes the cooperative core. Only hit - // the kernel when the shape actually CHANGES. - static LAST_CURSOR_SHAPE: AtomicU32 = AtomicU32::new(u32::MAX); - let changed = LAST_CURSOR_SHAPE.swap(shape, Ordering::Relaxed) != shape; - if changed { - sys::cursor_shape_set(shape); - } - static CUR_LOG: AtomicU32 = AtomicU32::new(0); - if CUR_LOG.fetch_add(1, Ordering::Relaxed) < 16 { - write(b"[embedder/cursor] kind="); - write(&kind[..klen]); - write(b" -> shape="); - write_dec(shape as u64); - write(b"\n"); - } - } - } - } + // args = { device, kind }. Map the Flutter cursor `kind` to a kernel cursor + // shape and apply it via SYS_CURSOR_SHAPE_SET so the compositor draws the + // matching pointer (hand on links, I-beam on text…). The request is binary + // StandardMethodCodec, so decode it as such (the old JSON parse silently + // failed → cursor shapes never changed). + if let Some(kind) = std_find_kind(payload) { + let shape = cursor_kind_to_shape(kind); + // De-dupe: Flutter spams activateSystemCursor on every hover move. The + // kernel syscall redraws the cursor (invalidate → composite), so calling + // it per-event is a redraw STORM. Only hit the kernel on a real change. + static LAST_CURSOR_SHAPE: AtomicU32 = AtomicU32::new(u32::MAX); + let changed = LAST_CURSOR_SHAPE.swap(shape, Ordering::Relaxed) != shape; + if changed { + sys::cursor_shape_set(shape); + } + static CUR_LOG: AtomicU32 = AtomicU32::new(0); + if CUR_LOG.fetch_add(1, Ordering::Relaxed) < 16 { + write(b"[embedder/cursor] kind="); + write(kind); + write(b" -> shape="); + write_dec(shape as u64); + write(b"\n"); } } - respond_platform_message(msg, JSON_SUCCESS_NULL); + respond_platform_message(msg, METHOD_SUCCESS_NULL); } /// Map a Flutter `SystemMouseCursors` kind string to a kernel `CURSOR_SHAPE_*`. @@ -2640,6 +3136,23 @@ extern "C" fn main_embedder(host_mode: u64, app_id: u64, aot_va: u64) { static ARG2: &[u8] = b"--enable-software-rendering=true\0"; static ARG3: &[u8] = b"--disable-vm-service\0"; static ARG4: &[u8] = b"--precompiled-mode\0"; + // SERIAL-GC FIX (post-app-launch freeze). The default Dart VM runs parallel + // GC worker threads (scavenger + marker) plus concurrent mark/sweep threads; + // a collection is a stop-the-world safepoint that needs ALL of them to park + // simultaneously. On our cooperative single-core scheduler that safepoint can + // never converge for a 2nd app engine → its render thread freezes (the live + // repro: launch Files, sweep, present_callback stops advancing). Making GC + // fully SERIAL — run entirely on the mutator that triggered it, no background + // GC threads to coordinate — removes the safepoint convergence problem at its + // root. These flags ARE forwarded now: the AOT engine is rebuilt with + // engine-port/patches/0004 adding them to switches.cc kAllowedDartFlags + // (prefix-matched, so the "=value" forms pass), and --dart-flags is parsed + // unconditionally (not gated on !FLUTTER_RELEASE). DELIVERED via the single + // --dart-flags= switch (bare --flag argv items are NOT forwarded to the VM). + // AOT-only: the JIT/debug VM (x86) asserts num_tasks>0 and SIGABRTs on + // tasks=0, so the JIT path keeps the bare heap args below instead (ARG5-ARG7). + static ARG_DARTFLAGS_AOT: &[u8] = + b"--dart-flags=--scavenger_tasks=0,--marker_tasks=0,--no_concurrent_mark,--no_concurrent_sweep\0"; static ARG5: &[u8] = b"--old_gen_heap_size=64\0"; static ARG6: &[u8] = b"--new_gen_heap_size=8\0"; static ARG7: &[u8] = b"--max_old_gen_heap_size=64\0"; @@ -2684,7 +3197,21 @@ extern "C" fn main_embedder(host_mode: u64, app_id: u64, aot_va: u64) { ARG5.as_ptr(), ARG6.as_ptr(), ARG7.as_ptr(), - ARG8.as_ptr(), + ]); + // AOT/release argv: the 5 AOT-safe switches + the serial-GC --dart-flags. The + // patched release engine (both arches now) forwards the GC flags to the Dart + // VM — the post-app-launch freeze fix. + #[repr(transparent)] + struct ArgvPtrs6([*const u8; 6]); + unsafe impl Sync for ArgvPtrs6 {} + static ENGINE_ARGV_AOT: ArgvPtrs6 = + ArgvPtrs6([ + ARG0.as_ptr(), + ARG1.as_ptr(), + ARG2.as_ptr(), + ARG3.as_ptr(), + ARG4.as_ptr(), + ARG_DARTFLAGS_AOT.as_ptr(), ]); let mut project_args = FlutterProjectArgsRaw { @@ -2706,10 +3233,20 @@ extern "C" fn main_embedder(host_mode: u64, app_id: u64, aot_va: u64) { icu_path.as_ptr() as u64, ); // ARG0-ARG4 are AOT-safe (name, impeller=false, software-rendering, - // disable-vm-service, precompiled-mode). ARG5-ARG8 are JIT-era GC/heap - // workarounds that the RELEASE/AOT engine rejects as disallowed Dart VM flags - // (switches.cc:478 FATAL). So pass only the first 5 args under AOT. - let engine_argc: i32 = if is_aot { 5 } else { ENGINE_ARGV.0.len() as i32 }; + // disable-vm-service, precompiled-mode). Under AOT on aarch64 we additionally + // pass the serial-GC --dart-flags switch (ENGINE_ARGV_AOT, argc=6) — the + // patched arm64 release engine forwards it to the Dart VM, which is the + // post-app-launch freeze fix (verified on aarch64). + // + // Both arches now run the patched release engine in AOT mode with prebuilt + // product-mode libapp.so snapshots, so both deliver the serial-GC --dart-flags + // (ENGINE_ARGV_AOT, argc=6). JIT path (no AOT snapshot) keeps the full 8-arg + // ENGINE_ARGV with bare heap flags — its debug VM asserts on tasks=0. + let (argv_ptr, engine_argc): (u64, i32) = if is_aot { + (ENGINE_ARGV_AOT.0.as_ptr() as u64, 6) + } else { + (ENGINE_ARGV.0.as_ptr() as u64, ENGINE_ARGV.0.len() as i32) + }; write_i32_at( &mut project_args.bytes, OFF_PROJECT_ARGS_COMMAND_LINE_ARGC, @@ -2718,7 +3255,7 @@ extern "C" fn main_embedder(host_mode: u64, app_id: u64, aot_va: u64) { write_u64_at( &mut project_args.bytes, OFF_PROJECT_ARGS_COMMAND_LINE_ARGV, - ENGINE_ARGV.0.as_ptr() as u64, + argv_ptr, ); write_u64_at( &mut project_args.bytes, diff --git a/tools/flutter-embedder/src/sys.rs b/tools/flutter-embedder/src/sys.rs index b4ba9c2..dc14d2b 100644 --- a/tools/flutter-embedder/src/sys.rs +++ b/tools/flutter-embedder/src/sys.rs @@ -32,6 +32,10 @@ pub const SYS_SURFACE_CREATE: u64 = 0x300; pub const SYS_SURFACE_UPLOAD_RGBA32: u64 = 0x303; pub const SYS_SURFACE_DESTROY: u64 = 0x302; pub const SYS_SURFACE_FLIP: u64 = 0x312; +pub const SYS_SURFACE_Z_SET: u64 = 0x30A; +pub const SYS_SURFACE_GEOMETRY_SET: u64 = 0x30C; +pub const SYS_SURFACE_VISIBILITY_SET: u64 = 0x30E; +pub const SYS_SURFACE_CLIP_SET: u64 = 0x30F; pub const SYS_FB_SIZE_PACKED: u64 = 0x305; pub const SYS_VSYNC_WAIT_NONBLOCK: u64 = 0x307; @@ -85,6 +89,15 @@ pub const SYS_CLIPBOARD_GET: u64 = 0x4B4; pub const SYS_APP_CLOSE_FOREGROUND: u64 = 0x4B5; pub const SYS_BEEP: u64 = 0x4B6; +// App networking — TCP socket calls (mirrors abi.rs / dispatch.rs). Connect is +// async: poll SYS_TCP_STATUS until it returns 1 before writing. +pub const SYS_TCP_CONNECT: u64 = 0x388; +pub const SYS_TCP_WRITE: u64 = 0x389; +pub const SYS_TCP_READ: u64 = 0x38A; +pub const SYS_TCP_CLOSE: u64 = 0x38B; +pub const SYS_TCP_STATUS: u64 = 0x4B7; +pub const SYS_DNS_RESOLVE: u64 = 0x4B8; + // Mouse-cursor shapes accepted by SYS_CURSOR_SHAPE_SET (mirrors abi.rs). pub const CURSOR_SHAPE_BASIC: u32 = 0; pub const CURSOR_SHAPE_TEXT: u32 = 1; @@ -457,6 +470,30 @@ pub fn surface_destroy(surface_id: u32) -> i64 { unsafe { syscall1(SYS_SURFACE_DESTROY, surface_id as u64) } } +/// Set a surface's stacking order (higher z draws on top). +pub fn surface_z_set(surface_id: u32, z: i32) -> i64 { + unsafe { syscall2(SYS_SURFACE_Z_SET, surface_id as u64, (z as u32) as u64) } +} + +/// Set a surface's on-screen position + size. (xy/wh packed as the kernel expects.) +pub fn surface_geometry_set(surface_id: u32, x: i32, y: i32, w: u32, h: u32) -> i64 { + let xy = (((x as u32) as u64) << 32) | ((y as u32) as u64); + let wh = ((w as u64) << 32) | (h as u64); + unsafe { syscall3(SYS_SURFACE_GEOMETRY_SET, surface_id as u64, xy, wh) } +} + +/// Clip a surface to the given screen-space rect. +pub fn surface_clip_set(surface_id: u32, x: i32, y: i32, w: u32, h: u32) -> i64 { + let xy = (((x as u32) as u64) << 32) | ((y as u32) as u64); + let wh = ((w as u64) << 32) | (h as u64); + unsafe { syscall3(SYS_SURFACE_CLIP_SET, surface_id as u64, xy, wh) } +} + +/// Show or hide a surface. +pub fn surface_visibility_set(surface_id: u32, visible: bool) -> i64 { + unsafe { syscall2(SYS_SURFACE_VISIBILITY_SET, surface_id as u64, visible as u64) } +} + /// Returns `(width << 32) | height` for the physical framebuffer. pub fn fb_size_packed() -> u64 { let ret = unsafe { syscall0(SYS_FB_SIZE_PACKED) }; @@ -897,3 +934,40 @@ pub fn beep(freq_hz: u32, duration_ms: u32) -> i64 { unsafe { syscall2(SYS_BEEP, freq_hz as u64, duration_ms as u64) } } +// ── App networking — TCP socket calls (back the oscortex/net channel) ───────── + +/// Open a TCP connection (async). `ip` is big-endian (network order); returns a +/// socket fd ≥ 0, or a negative errno. Poll `tcp_status` until it returns 1 +/// before writing. +pub fn tcp_connect(ip_be: u32, port: u16) -> i64 { + unsafe { syscall2(SYS_TCP_CONNECT, ip_be as u64, port as u64) } +} + +/// Poll an outbound connect: 1 = established (may send), 0 = still connecting, +/// negative = refused/reset. +pub fn tcp_status(fd: u32) -> i64 { + unsafe { syscall1(SYS_TCP_STATUS, fd as u64) } +} + +/// Write to a TCP socket. Returns bytes written, or a negative errno +/// (-11 EAGAIN if the send buffer is momentarily full). +pub fn tcp_write(fd: u32, data: &[u8]) -> i64 { + unsafe { syscall3(SYS_TCP_WRITE, fd as u64, data.as_ptr() as u64, data.len() as u64) } +} + +/// Read from a TCP socket. Returns bytes read, or a negative errno +/// (-11 EAGAIN if no data is available yet). +pub fn tcp_read(fd: u32, buf: &mut [u8]) -> i64 { + unsafe { syscall3(SYS_TCP_READ, fd as u64, buf.as_mut_ptr() as u64, buf.len() as u64) } +} + +/// Close a TCP socket. +pub fn tcp_close(fd: u32) -> i64 { + unsafe { syscall1(SYS_TCP_CLOSE, fd as u64) } +} + +/// Resolve a hostname → IPv4 (big-endian u32 ≥ 0), or a negative errno. +pub fn dns_resolve(name: &[u8]) -> i64 { + unsafe { syscall2(SYS_DNS_RESOLVE, name.as_ptr() as u64, name.len() as u64) } +} + diff --git a/tools/osx-cli/.gitignore b/tools/osx-cli/.gitignore new file mode 100644 index 0000000..7265e1f --- /dev/null +++ b/tools/osx-cli/.gitignore @@ -0,0 +1,10 @@ +# Dart / pub +.dart_tool/ +.packages +pubspec.lock +build/ + +# IDE +.idea/ +*.iml +.vscode/ diff --git a/tools/osx-cli/CHANGELOG.md b/tools/osx-cli/CHANGELOG.md new file mode 100644 index 0000000..7c9937f --- /dev/null +++ b/tools/osx-cli/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +## 0.1.0 + +Initial release. + +- `osx init` — fetch the prebuilt OSCortex engine + gen_snapshot for both + architectures and record per-app config. +- `osx build` — compile a Flutter app into a universal `.osx` bundle (native + aarch64 + x86_64) plus per-arch bundles. +- `osx run` / `osx preview` — boot OSCortex in QEMU, with hardware acceleration + for the host arch and emulation for the other. diff --git a/tools/osx-cli/LICENSE b/tools/osx-cli/LICENSE new file mode 100644 index 0000000..dba13ed --- /dev/null +++ b/tools/osx-cli/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/tools/osx-cli/README.md b/tools/osx-cli/README.md new file mode 100644 index 0000000..a55381f --- /dev/null +++ b/tools/osx-cli/README.md @@ -0,0 +1,206 @@ +# osx — the OSCortex app toolchain + +Build and preview **Flutter apps for [OSCortex](https://github.com/DotCorr/oscortex)**, +a Rust microkernel with a Flutter shell. OSCortex apps ship as **`.osx` +bundles**. Flutter's own toolchain doesn't target OSCortex, so this package adds +the missing `init` / `build` / `run` steps and hides the internals. + +A single `osx build` produces a **universal bundle that is native for both +`aarch64` and `x86_64`** — app developers never pick an architecture, and never +touch the engine, `gen_snapshot`, or the bundle format by hand. + +``` +osx init set up the OSCortex build target for a Flutter app +osx build compile the app into a universal .osx bundle (both arches) +osx run boot OSCortex in an emulator to preview (host arch fast, other emulated) +``` + +--- + +## Install + +```bash +dart pub global activate oscortex_cli +``` + +This puts an `osx` executable on your path (ensure `~/.pub-cache/bin` is in +`PATH`). To run from a checkout instead: + +```bash +cd tools/osx-cli +dart pub get +dart run bin/osx.dart --help +``` + +### Prerequisites + +| Tool | Needed for | Notes | +|------|-----------|-------| +| **Flutter** (3.41.x) | `init`, `build` | Produces the asset bundle + AOT kernel. Must be on `PATH`. | +| **Docker** | `build` | Runs the per-arch `gen_snapshot` (Linux ELF) in a container. | +| **QEMU** | `run` | `qemu-system-aarch64` and/or `qemu-system-x86_64`. | +| **OSCortex source tree** | all | The CLI orchestrates scripts in the repo. See [Locating the OSCortex tree](#locating-the-oscortex-tree). | + +`osx init` checks all of these and tells you what's missing. + +--- + +## Commands + +### `osx init` + +Run from the root of your Flutter app. It: + +1. Confirms you're in a Flutter project. +2. Locates the OSCortex source tree (records it for later commands). +3. Verifies the toolchain (Flutter, Docker, QEMU, host arch + accelerator). +4. Fetches the pinned prebuilt engine **and `gen_snapshot` for both + architectures** (via the repo's `engine-port/fetch-engine.sh`; the pin lives + in `engine-port/artifact.config`). +5. Writes `.osx/config.json` (display name, engine pin, OSCortex tree, version). + +```bash +cd my_flutter_app +osx init +osx init --name "My App" # override the launcher display name +osx init --skip-fetch # record config + verify only (no download) +``` + +### `osx build` + +Compiles the current app into `.osx` bundles. The pipeline: + +``` +host flutter pub get +host flutter build bundle -> build/flutter_assets/ +host frontend_server --aot --tfa -> build/app_aot.dill (arch-neutral) +docker gen_snapshot[arm64] (linux/arm64) -> libapp-arm64.so +docker gen_snapshot[x64] (linux/amd64) -> libapp-x64.so +pack per-arch My App-arm64.osx / My App-x64.osx +pack universal My App.osx (both snapshots in one bundle) +``` + +```bash +osx build # both arches + the universal bundle +osx build --arch arm64 # one arch only (no universal bundle) +osx build --name "My App" # override display name +osx build --output-dir dist # write bundles somewhere else +osx build --no-assets # skip staging build/flutter_assets +``` + +Outputs land in `build/osx/` by default: + +``` +build/osx/ + My App.osx ← universal (fat) bundle — ship this + My App-arm64.osx ← aarch64-only bundle + My App-x64.osx ← x86_64-only bundle + flutter_assets/ ← fonts, manifests, shaders (arch-neutral) +``` + +> The AOT kernel (`app_aot.dill`) is architecture-neutral and built once on the +> host. Only `gen_snapshot` is per-arch, and it's a Linux binary, so `build` +> runs it in a throwaway `ubuntu:22.04` container with the matching +> `--platform`. On Apple Silicon the `x86_64` snapshot is produced under +> emulation (correct, just slower). + +### `osx run` (alias `osx preview`) + +Boots OSCortex in QEMU so you can preview across architectures. The **host-native +arch runs with hardware acceleration** (HVF on macOS, KVM on Linux); the **other +arch is emulated** with TCG. + +```bash +osx run # boot the host arch, accelerated +osx run --arch x64 # boot x86_64 (emulated on an arm64 host) +osx run --arch arm64 # boot aarch64 (emulated on an x86_64 host) +osx run --image path/to.iso # boot a specific image +osx run --headless # no window; serial on stdio +osx run --serial-log /tmp/s.log # capture guest serial to a file +osx run --release v0.1.2 # pick the release the ISO is fetched from +``` + +Image selection, in order: + +1. `--image ` if given. +2. A local `aarch64` `.kernel` (checked under `~/OSCortex-run/`, or + `OSCORTEX_KERNEL`) — the fastest, most reliable interactive path on Apple + Silicon. +3. Otherwise the release **`.iso`** for the target arch, downloaded and cached + under `/.image-cache/`. + +In the window: click to capture the mouse, `Ctrl+Alt+G` to release, close the +window (or `Ctrl-A X` when headless) to quit. + +--- + +## The universal `.osx` bundle + +OSCortex `.osx` bundles start with a fixed 112-byte header (`OSCP` magic, name, +version, and a single AOT snapshot length) — see the kernel's app registry. That +v1 layout carries **one** snapshot. + +`osx build` emits a **universal bundle** that is a strict, backward-compatible +superset: + +``` +offset 0 ┌───────────────────────────────┐ + │ v1 header (OSCP, version 1) │ ← unchanged; aot_len covers the + │ aot_len = primary snapshot │ host-arch (primary) snapshot +offset 112 ├───────────────────────────────┤ + │ primary AOT snapshot │ ← an unmodified OS boots THIS arch + ├───────────────────────────────┤ + │ OSXM index │ ← (arch token, offset, length)×N + │ other AOT snapshot(s) │ + └───────────────────────────────┘ +``` + +* An **OS that only understands v1** reads the header and the first snapshot — it + always boots the primary (host) arch, so a fat bundle never bricks an older + build. +* An **arch-aware loader** reads the `OSXM` index and picks the matching arch at + install/run time. + +For maximum compatibility today, `build` also writes plain per-arch v1 bundles +(`-arm64.osx` / `-x64.osx`). Their bytes are identical to what the project's +`tools/oscortex-pack.py` produces. See +[Status](#status--known-limitations) for the kernel-side selection that's still +pending. + +--- + +## Locating the OSCortex tree + +`osx` orchestrates scripts inside the OSCortex repo. It finds the tree via, in +order: + +1. `--oscortex-root ` +2. `OSCORTEX_ROOT` environment variable +3. `oscortexRoot` recorded in your app's `.osx/config.json` (written by `init`) +4. The repo this CLI itself lives in (when run from a checkout) + +```bash +export OSCORTEX_ROOT=~/src/oscortex +# or +osx init --oscortex-root ~/src/oscortex +``` + +--- + +## Status / known limitations + +| Area | State | +|------|-------| +| `osx init` (fetch both engines, config, prereqs) | **Works.** | +| `osx build` (both arches + universal bundle) | **Works.** Per-arch v1 bundles are byte-identical to `oscortex-pack.py`; the universal bundle is verified v1-readable. | +| `osx run` (HVF/KVM host arch + TCG other arch) | **Works** for both arches; auto-fetches the release ISO; prefers a local `.kernel` on aarch64. | +| Kernel-side **arch selection from the fat bundle** | **Pending.** Today's kernel reads only the v1 header (one arch). The universal bundle's `OSXM` index is the forward path; until the kernel learns to select, install the per-arch bundle that matches the target. | +| `osx run` **side-loading a freshly built app** on boot | **Not yet.** `run` boots the OS image (shell + core apps). Installing your own `.osx` into a running guest uses the package pipeline. | +| `build` without Docker | Not supported — `gen_snapshot` is a Linux binary. A native macOS/Windows `gen_snapshot` would remove this dependency. | + +--- + +## License + +Dual-licensed: AGPL-3.0-or-later **or** a commercial license. See the `LICENSE` +and `LICENSE-COMMERCIAL` files in the OSCortex repository. diff --git a/tools/osx-cli/analysis_options.yaml b/tools/osx-cli/analysis_options.yaml new file mode 100644 index 0000000..bb02492 --- /dev/null +++ b/tools/osx-cli/analysis_options.yaml @@ -0,0 +1,7 @@ +include: package:lints/recommended.yaml + +linter: + rules: + - prefer_single_quotes + - directives_ordering + - unawaited_futures diff --git a/tools/osx-cli/bin/osx.dart b/tools/osx-cli/bin/osx.dart new file mode 100644 index 0000000..202c51a --- /dev/null +++ b/tools/osx-cli/bin/osx.dart @@ -0,0 +1,7 @@ +import 'dart:io'; + +import 'package:oscortex_cli/oscortex_cli.dart' as cli; + +Future main(List args) async { + exitCode = await cli.run(args); +} diff --git a/tools/osx-cli/lib/oscortex_cli.dart b/tools/osx-cli/lib/oscortex_cli.dart new file mode 100644 index 0000000..39202d7 --- /dev/null +++ b/tools/osx-cli/lib/oscortex_cli.dart @@ -0,0 +1,67 @@ +import 'dart:io'; + +import 'package:args/args.dart'; +import 'package:args/command_runner.dart'; + +import 'src/commands/build_command.dart'; +import 'src/commands/init_command.dart'; +import 'src/commands/run_command.dart'; +import 'src/context.dart'; +import 'src/util/errors.dart'; +import 'src/util/host.dart'; +import 'src/util/logger.dart'; + +/// Package version, surfaced via `osx --version`. Keep in sync with pubspec. +const osxCliVersion = '0.1.0'; + +/// Entry point used by `bin/osx.dart`. Returns the process exit code. +Future run(List args) async { + final log = Logger(); + final ctx = CliContext(log: log, host: Host.detect()); + + final runner = OsxCommandRunner(ctx); + try { + final code = await runner.run(args); + return code ?? 0; + } on UsageException catch (e) { + stderr.writeln(e.message); + stderr.writeln(); + stderr.writeln(e.usage); + return 64; // EX_USAGE + } on CliException catch (e) { + log.error(e.message); + if (e.hint != null) log.detail('→ ${e.hint}'); + return e.exitCode; + } +} + +class OsxCommandRunner extends CommandRunner { + OsxCommandRunner(this._ctx) + : super( + 'osx', + 'Build and run Flutter apps for OSCortex.\n\n' + 'Compile a Flutter app into a universal .osx bundle (native\n' + 'aarch64 + x86_64) and preview it across architectures in QEMU.') { + argParser + ..addFlag('verbose', + abbr: 'v', negatable: false, help: 'Print diagnostic (trace) output.') + ..addFlag('version', + negatable: false, help: 'Print the osx CLI version and exit.'); + + addCommand(InitCommand(_ctx)); + addCommand(BuildCommand(_ctx)); + addCommand(RunCommand(_ctx)); + } + + final CliContext _ctx; + + @override + Future runCommand(ArgResults topLevelResults) async { + if (topLevelResults['version'] as bool) { + stdout.writeln('osx $osxCliVersion'); + return 0; + } + _ctx.log.verbose = topLevelResults['verbose'] as bool; + return super.runCommand(topLevelResults); + } +} diff --git a/tools/osx-cli/lib/src/commands/build_command.dart b/tools/osx-cli/lib/src/commands/build_command.dart new file mode 100644 index 0000000..2dc2f61 --- /dev/null +++ b/tools/osx-cli/lib/src/commands/build_command.dart @@ -0,0 +1,371 @@ +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:args/command_runner.dart'; +import 'package:path/path.dart' as p; + +import '../context.dart'; +import '../engine.dart'; +import '../osx_bundle.dart'; +import '../util/app_config.dart'; +import '../util/errors.dart'; +import '../util/host.dart'; +import '../util/oscortex_repo.dart'; + +/// `osx build` — compile the current Flutter app into `.osx` bundles that are +/// native for both aarch64 and x86_64. +/// +/// Pipeline (mirrors tools/build-flutter-osx.sh + engine-port/compile-app-aot.sh, +/// generalised to emit BOTH arches): +/// +/// host : flutter pub get +/// host : flutter build bundle -> build/flutter_assets/ (+ kernel_blob) +/// host : frontend_server --aot --tfa -> build/app_aot.dill (arch-neutral) +/// docker : gen_snapshot[arm64] (--platform linux/arm64) -> libapp-arm64.so +/// docker : gen_snapshot[x64] (--platform linux/amd64) -> libapp-x64.so +/// host : patch_libapp.py (x64 macos->linux feature tag, x64 only) +/// pack : per-arch -{arm64,x64}.osx + universal .osx +class BuildCommand extends Command { + BuildCommand(this._ctx) { + argParser + ..addMultiOption('arch', + allowed: ['arm64', 'aarch64', 'x64', 'x86_64'], + help: 'Limit to specific arch(es). Repeatable.\n' + '(default: both — required for a universal bundle)') + ..addOption('name', help: 'Override the display name in the bundle.') + ..addOption('output-dir', + help: 'Directory for the .osx outputs.\n(default: /build/osx)') + ..addFlag('assets', + defaultsTo: true, + help: 'Also stage build/flutter_assets next to the bundle.') + ..addFlag('universal', + defaultsTo: true, + help: + 'Emit the universal (fat) bundle in addition to per-arch ones.\n' + 'Requires both arches.'); + } + + final CliContext _ctx; + + @override + String get name => 'build'; + + @override + String get description => + 'Compile the current Flutter app into a universal .osx bundle\n' + '(native aarch64 + x86_64), plus per-arch bundles.'; + + @override + String get invocation => 'osx build [options]'; + + @override + Future run() async { + final log = _ctx.log; + final appDir = Directory.current.absolute.path; + + final cfg = AppConfig.load(appDir); + if (cfg == null) { + throw CliException( + 'This app is not initialised for OSCortex.', + hint: 'Run `osx init` first.', + ); + } + if (!File(p.join(appDir, 'lib', 'main.dart')).existsSync()) { + throw CliException('No lib/main.dart in $appDir — not a Flutter app.'); + } + + final repo = + OscortexRepo.resolve(override: cfg.oscortexRoot, appConfig: cfg.asMap); + final engine = EngineManager(repo, log, _ctx.proc); + + // Which arches to build. + final requested = (argResults!['arch'] as List) + .map(Arch.parse) + .whereType() + .toSet(); + final arches = requested.isEmpty ? Arch.values.toSet() : requested; + + final displayName = (argResults!['name'] as String?) ?? + cfg.displayName ?? + p.basename(appDir); + final version = cfg.bundleVersion ?? '1.0.0'; + + if (!await _dockerAvailable()) { + throw CliException( + 'Docker is required to run the per-arch gen_snapshot.', + hint: 'Install Docker Desktop (or a Linux docker daemon) and retry. ' + 'The gen_snapshot binaries are Linux ELF and run in a container.', + ); + } + + // Ensure engines/gen_snapshots are present for each target arch. + for (final a in arches) { + await engine.fetch(a); + } + + // 1. pub get + asset bundle (host). + log.info('flutter pub get'); + await _ctx.proc.run('flutter', ['pub', 'get'], workingDirectory: appDir); + + log.info('Building Flutter asset bundle (flutter build bundle)'); + await _ctx.proc.run( + 'flutter', + ['--suppress-analytics', 'build', 'bundle'], + workingDirectory: appDir, + ); + + // 2. AOT dill (host, arch-neutral). + final dill = File(p.join(appDir, 'build', 'app_aot.dill')); + await _compileAotDill(appDir, dill); + + final outDir = (argResults!['output-dir'] as String?) ?? + p.join(appDir, 'build', 'osx'); + + // 3. Per-arch gen_snapshot (docker). + final snapshots = {}; + for (final a in arches) { + final so = await _genSnapshot(repo, engine, a, dill, appDir, outDir); + snapshots[a] = so; + } + + // 4. Pack. + final outputs = OsxOutputs(outDir, displayName); + + for (final a in arches) { + OsxBundle.writeSingleArch( + snapshot: snapshots[a]!, + name: displayName, + version: version, + out: outputs.perArch[a]!, + ); + log.ok('Bundle (${a.label}): ' + '${p.relative(outputs.perArch[a]!.path)} ' + '(${_human(outputs.perArch[a]!.lengthSync())})'); + } + + final wantUniversal = argResults!['universal'] as bool; + if (wantUniversal && arches.length > 1) { + // Host arch is the primary slot so an unmodified kernel boots it. + final primary = + _ctx.host.arch != null && snapshots.containsKey(_ctx.host.arch) + ? _ctx.host.arch! + : snapshots.keys.first; + OsxBundle.writeUniversal( + snapshots: snapshots, + primary: primary, + name: displayName, + version: version, + out: outputs.universal, + ); + log.ok('Universal bundle: ${p.relative(outputs.universal.path)} ' + '(${_human(outputs.universal.lengthSync())}, primary=${primary.label})'); + } else if (wantUniversal) { + log.warn('Universal bundle skipped — needs both arches ' + '(built: ${arches.map((a) => a.label).join(', ')}).'); + } + + // 5. Assets (optional). + if (argResults!['assets'] as bool) { + _stageAssets(appDir, outDir); + } + + log.ok('Done. Preview with `osx run`.'); + return 0; + } + + // --- pipeline steps --- + + Future _compileAotDill(String appDir, File dill) async { + _ctx.log.info('Compiling AOT kernel (frontend_server --aot --tfa)'); + final fl = _flutterRoot(); + final dartaot = + p.join(fl, 'bin', 'cache', 'dart-sdk', 'bin', 'dartaotruntime'); + final frontend = p.join(fl, 'bin', 'cache', 'artifacts', 'engine', + 'darwin-x64', 'frontend_server_aot.dart.snapshot'); + // Product patched SDK — OSCortex runs the product engine; a release/profile + // dill is rejected at load. (Matches engine-port/compile-app-aot.sh.) + final sdkProduct = p.join(fl, 'bin', 'cache', 'artifacts', 'engine', + 'common', 'flutter_patched_sdk_product') + + Platform.pathSeparator; + final pkgConfig = p.join(appDir, '.dart_tool', 'package_config.json'); + + for (final f in [dartaot, frontend]) { + if (!File(f).existsSync()) { + throw CliException('Missing Flutter AOT tool: $f', + hint: 'Run `flutter precache` or reinstall Flutter.'); + } + } + + dill.parent.createSync(recursive: true); + await _ctx.proc.run( + dartaot, + [ + frontend, + '--sdk-root', + sdkProduct, + '--target=flutter', + '--aot', + '--tfa', + '--packages=$pkgConfig', + '--output-dill', + dill.path, + p.join(appDir, 'lib', 'main.dart'), + ], + workingDirectory: appDir, + what: 'frontend_server --aot', + ); + if (!dill.existsSync()) { + throw CliException('AOT kernel compilation produced no dill.'); + } + } + + /// Run the arch-matched (Linux) gen_snapshot inside docker on [dill], + /// returning the resulting libapp ELF bytes. + Future _genSnapshot( + OscortexRepo repo, + EngineManager engine, + Arch arch, + File dill, + String appDir, + String outDir, + ) async { + _ctx.log.info( + 'gen_snapshot (${arch.label}) — app_aot.dill -> libapp-${arch.osxToken}.so'); + + final genSnap = engine.genSnapshot(arch); + if (!genSnap.existsSync()) { + throw CliException( + 'gen_snapshot missing for ${arch.label}: ${genSnap.path}', + hint: 'Re-run `osx init` (engine ${engine.config.artifactVersion} ' + 'should ship a per-arch gen_snapshot).', + ); + } + + final outSo = File(p.join(outDir, 'libapp-${arch.osxToken}.so')); + outSo.parent.createSync(recursive: true); + + final platform = arch == Arch.aarch64 ? 'linux/arm64' : 'linux/amd64'; + + // Bind-mount every host tree the container must read/write, with identical + // in-container paths (so the absolute paths we pass just work): + // - the repo root: gen_snapshot lives under .engine-cache there + // - the app dir: the AOT dill is under /build + // - the output dir: where we write libapp-.so + // De-duplicate nested paths so docker doesn't reject overlapping mounts. + final mounts = _dockerMounts([repo.root, appDir, outSo.parent.path]); + + await _ctx.proc.run( + 'docker', + [ + 'run', + '--rm', + '--platform', + platform, + ...mounts, + '-w', + repo.root, + 'ubuntu:22.04', + genSnap.path, + '--deterministic', + '--snapshot_kind=app-aot-elf', + '--elf=${outSo.path}', + '--strip', + dill.path, + ], + what: 'docker gen_snapshot ($platform)', + ); + + if (!outSo.existsSync() || outSo.lengthSync() < 1024) { + throw CliException( + 'gen_snapshot produced no usable ELF for ${arch.label}.'); + } + + // x64-only: the macOS-host frontend_server tags the snapshot "x64 macos ..."; + // the Linux engine expects "x64 linux ...". patch_libapp.py rewrites it + // in-place (same byte length). arm64 is built natively on linux/arm64 and + // needs no patch. (Mirrors tools/build-flutter-osx.sh.) + if (arch == Arch.x86_64) { + final patch = + File(p.join(repo.root, 'tools', 'flutter-engine', 'patch_libapp.py')); + if (patch.existsSync()) { + await _ctx.proc + .run('python3', [patch.path, outSo.path], what: 'patch_libapp.py'); + } + } + + return outSo.readAsBytesSync(); + } + + void _stageAssets(String appDir, String outDir) { + final src = Directory(p.join(appDir, 'build', 'flutter_assets')); + if (!src.existsSync()) { + _ctx.log.warn('flutter_assets missing — skipping asset staging.'); + return; + } + final dst = Directory(p.join(outDir, 'flutter_assets')); + if (dst.existsSync()) dst.deleteSync(recursive: true); + dst.createSync(recursive: true); + for (final e in src.listSync(recursive: true)) { + final rel = p.relative(e.path, from: src.path); + final target = p.join(dst.path, rel); + if (e is Directory) { + Directory(target).createSync(recursive: true); + } else if (e is File) { + File(target).parent.createSync(recursive: true); + e.copySync(target); + } + } + _ctx.log.detail('staged flutter_assets -> ${p.relative(dst.path)}'); + } + + // --- helpers --- + + /// Build `-v host:host` mount args for [paths], dropping any path nested + /// inside another (docker rejects overlapping bind mounts). + List _dockerMounts(List paths) { + final normalized = paths.map((x) => p.normalize(p.absolute(x))).toSet(); + final roots = []; + for (final candidate in normalized) { + final coveredBy = normalized + .any((other) => other != candidate && p.isWithin(other, candidate)); + if (!coveredBy) roots.add(candidate); + } + return [ + for (final r in roots) ...['-v', '$r:$r'] + ]; + } + + Future _dockerAvailable() async { + final r = await _ctx.proc.run( + 'docker', + ['version', '--format', '{{.Server.Version}}'], + captureStdout: true, + allowFailure: true, + what: 'docker version', + ); + return r.exitCode == 0; + } + + String _flutterRoot() { + final env = Platform.environment['FLUTTER_ROOT'] ?? + Platform.environment['FLUTTER_HOME']; + if (env != null && Directory(env).existsSync()) return env; + // Derive from `which flutter` -> /bin/flutter. + try { + final r = Process.runSync('which', ['flutter']); + if (r.exitCode == 0) { + final binFlutter = (r.stdout as String).trim(); + // .../flutter/bin/flutter -> .../flutter + return p + .dirname(p.dirname(File(binFlutter).resolveSymbolicLinksSync())); + } + } catch (_) {/* fall through */} + return '/opt/homebrew/share/flutter'; + } + + String _human(int bytes) { + if (bytes < 1024) return '$bytes B'; + if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KiB'; + return '${(bytes / 1024 / 1024).toStringAsFixed(1)} MiB'; + } +} diff --git a/tools/osx-cli/lib/src/commands/init_command.dart b/tools/osx-cli/lib/src/commands/init_command.dart new file mode 100644 index 0000000..ed926b8 --- /dev/null +++ b/tools/osx-cli/lib/src/commands/init_command.dart @@ -0,0 +1,165 @@ +import 'dart:io'; + +import 'package:args/command_runner.dart'; +import 'package:path/path.dart' as p; + +import '../context.dart'; +import '../engine.dart'; +import '../util/app_config.dart'; +import '../util/errors.dart'; +import '../util/host.dart'; +import '../util/logger.dart'; +import '../util/oscortex_repo.dart'; +import '../util/proc.dart'; + +/// `osx init` — prepare a Flutter app directory to target OSCortex. +class InitCommand extends Command { + InitCommand(this._ctx) { + argParser + ..addOption('name', + help: 'Display name shown in the OSCortex launcher.\n' + '(defaults to the pubspec name)') + ..addOption('oscortex-root', + help: 'Path to the OSCortex source tree to record for this app.\n' + '(defaults to OSCORTEX_ROOT or the tree this CLI lives in)') + ..addFlag('skip-fetch', + negatable: false, + help: 'Record config + verify prerequisites without downloading ' + 'the engine.'); + } + + final CliContext _ctx; + + @override + String get name => 'init'; + + @override + String get description => + 'Set up the OSCortex build target for the current Flutter app:\n' + 'fetch the prebuilt engine + gen_snapshot (both arches), verify the\n' + 'toolchain, and record per-app config in .osx/config.json.'; + + @override + String get invocation => 'osx init [options]'; + + @override + Future run() async { + final log = _ctx.log; + final appDir = Directory.current.absolute.path; + + // 1. Confirm we're in a Flutter app. + final pubspec = File(p.join(appDir, 'pubspec.yaml')); + if (!pubspec.existsSync()) { + throw CliException( + 'No pubspec.yaml in $appDir.', + hint: 'Run `osx init` from the root of a Flutter app.', + ); + } + final pubspecText = pubspec.readAsStringSync(); + final pkgName = _yamlScalar(pubspecText, 'name') ?? p.basename(appDir); + final pkgVersion = _yamlScalar(pubspecText, 'version') ?? '1.0.0'; + + // 2. Locate the OSCortex tree (records it for build/run). + final repo = OscortexRepo.resolve( + override: argResults!['oscortex-root'] as String?, + ); + log.info('OSCortex tree: ${repo.root}'); + + // 3. Verify host prerequisites. + await _verifyPrereqs(repo); + + // 4. Fetch engine + gen_snapshot for BOTH arches (unless skipped). + final engine = EngineManager(repo, log, _ctx.proc); + final pinnedVersion = engine.config.artifactVersion; + if (argResults!['skip-fetch'] as bool) { + log.warn('Skipping engine fetch (--skip-fetch).'); + } else { + for (final arch in Arch.values) { + await engine.fetch(arch); + } + } + + // 5. Write per-app config. + final cfg = AppConfig( + appDir: appDir, + displayName: (argResults!['name'] as String?) ?? _humanize(pkgName), + oscortexRoot: repo.root, + engineVersion: pinnedVersion, + bundleVersion: _normalizeVersion(pkgVersion), + ); + cfg.save(); + log.ok('Wrote ${p.relative(cfg.file.path, from: appDir)}'); + log.detail('display name : ${cfg.displayName}'); + log.detail('engine : ${cfg.engineVersion}'); + log.detail('bundle ver : ${cfg.bundleVersion}'); + + // 6. .gitignore the build dir hint. + _ensureGitignore(appDir, log); + + log.ok('Ready. Build with `osx build`, preview with `osx run`.'); + return 0; + } + + Future _verifyPrereqs(OscortexRepo repo) async { + final log = _ctx.log; + final host = _ctx.host; + log.info('Verifying prerequisites ...'); + + // Flutter (host-native; produces the asset bundle + AOT dill). + if (!await Proc.exists('flutter')) { + final want = EngineManager(repo, log, _ctx.proc).config.flutterVersion; + throw CliException( + 'Flutter SDK not found on PATH.', + hint: 'Install Flutter${want != null ? ' $want' : ''} and ensure ' + '`flutter` is on PATH.', + ); + } + log.detail('flutter ✓'); + + // Docker (runs the Linux gen_snapshot for each arch during build). + if (!await Proc.exists('docker')) { + log.warn('Docker not found — `osx build` needs it to run the per-arch ' + 'gen_snapshot. `osx run` does not.'); + } else { + log.detail('docker ✓'); + } + + // QEMU (preview). + final qemuArm = await Proc.exists('qemu-system-aarch64'); + final qemuX86 = await Proc.exists('qemu-system-x86_64'); + if (qemuArm || qemuX86) { + log.detail('qemu ✓ ' + '(${[if (qemuArm) 'aarch64', if (qemuX86) 'x86_64'].join(', ')})'); + } else { + log.warn('QEMU not found — `osx run`/`osx preview` will be unavailable ' + 'until qemu-system-aarch64 / qemu-system-x86_64 are installed.'); + } + + log.detail('host : ${host.os}/${host.arch?.label ?? 'unknown-arch'}' + '${host.accelerator != null ? ' (accel: ${host.accelerator})' : ' (no hw accel)'}'); + } + + // --- small helpers --- + + String? _yamlScalar(String yaml, String key) { + final m = RegExp('^$key:\\s*(.+)\$', multiLine: true).firstMatch(yaml); + return m?.group(1)?.trim().replaceAll(RegExp('["\']'), ''); + } + + String _humanize(String pkg) => pkg + .split(RegExp('[_-]')) + .where((s) => s.isNotEmpty) + .map((s) => s[0].toUpperCase() + s.substring(1)) + .join(' '); + + // The .osx header version field is 15 bytes; strip a Flutter build suffix. + String _normalizeVersion(String v) => v.split('+').first; + + void _ensureGitignore(String appDir, Logger log) { + final gi = File(p.join(appDir, '.gitignore')); + const marker = '# OSCortex build artifacts'; + if (gi.existsSync() && gi.readAsStringSync().contains(marker)) return; + gi.writeAsStringSync('\n$marker\nbuild/osx/\n', mode: FileMode.append); + log.trace('appended $marker to .gitignore'); + } +} diff --git a/tools/osx-cli/lib/src/commands/run_command.dart b/tools/osx-cli/lib/src/commands/run_command.dart new file mode 100644 index 0000000..056f291 --- /dev/null +++ b/tools/osx-cli/lib/src/commands/run_command.dart @@ -0,0 +1,111 @@ +import 'dart:io'; + +import 'package:args/command_runner.dart'; +import 'package:path/path.dart' as p; + +import '../context.dart'; +import '../emulator.dart'; +import '../util/app_config.dart'; +import '../util/errors.dart'; +import '../util/host.dart'; +import '../util/oscortex_repo.dart'; + +/// `osx run` (alias `osx preview`) — boot OSCortex in QEMU for previewing. +/// +/// Picks hardware acceleration (HVF on macOS, KVM on Linux) when the requested +/// arch matches the host arch, and falls back to TCG emulation for the other +/// arch. Boots a local image if present, else downloads the release ISO. +class RunCommand extends Command { + RunCommand(this._ctx) { + argParser + ..addOption('arch', + allowed: ['arm64', 'aarch64', 'x64', 'x86_64'], + help: 'Architecture to boot.\n(default: the host arch, run fast)') + ..addOption('image', + help: 'Path to a specific .iso or .kernel image to boot.') + ..addOption('release', + help: 'Release tag to fetch the bootable ISO from.\n' + '(default: ${Emulator.defaultReleaseTag})') + ..addFlag('headless', + negatable: false, + help: 'Boot without a display window (serial on stdio).') + ..addOption('serial-log', + help: 'Write the guest serial output to this file.') + ..addOption('memory', help: 'Guest RAM in MiB.', defaultsTo: '4096'); + } + + final CliContext _ctx; + + @override + String get name => 'run'; + + @override + List get aliases => const ['preview']; + + @override + String get description => + 'Boot OSCortex in an emulator (QEMU) to preview across architectures.\n' + 'Host-native arch runs accelerated (HVF/KVM); the other is emulated.\n' + 'Also available as `osx preview`.'; + + @override + String get invocation => 'osx run [options]'; + + @override + Future run() async { + final log = _ctx.log; + final host = _ctx.host; + + // Choose target arch: explicit flag, else host arch (fast path). + final reqArch = argResults!['arch'] as String?; + final arch = reqArch != null + ? Arch.parse(reqArch)! + : (host.arch ?? + (throw CliException( + 'Could not detect host architecture; pass --arch.', + ))); + + final memMiB = int.tryParse(argResults!['memory'] as String) ?? 4096; + final emulator = Emulator(host, log, _ctx.proc); + + // Resolve the OSCortex tree (optional here) to find a cache dir + local + // kernel hint. Falls back to a temp cache if no tree is configured. + String cacheDir; + String? localKernel; + final cfg = AppConfig.load(Directory.current.absolute.path); + try { + final repo = OscortexRepo.resolve( + override: cfg?.oscortexRoot, + appConfig: cfg?.asMap, + ); + cacheDir = p.join(repo.root, '.image-cache'); + } on CliException { + cacheDir = p.join(Directory.systemTemp.path, 'oscortex-images'); + } + localKernel = _envKernel(); + + final image = await emulator.resolveImage( + arch: arch, + explicit: argResults!['image'] as String?, + localKernel: localKernel, + releaseTag: + (argResults!['release'] as String?) ?? Emulator.defaultReleaseTag, + cacheDir: cacheDir, + ); + + log.info('Image: ${image.path} (${image.kind})'); + + await emulator.boot( + image, + headless: argResults!['headless'] as bool, + serialLog: argResults!['serial-log'] as String?, + memMiB: memMiB, + ); + return 0; + } + + String? _envKernel() { + final k = Platform.environment['OSCORTEX_KERNEL']; + return (k != null && File(k).existsSync()) ? k : null; + } +} diff --git a/tools/osx-cli/lib/src/context.dart b/tools/osx-cli/lib/src/context.dart new file mode 100644 index 0000000..5693025 --- /dev/null +++ b/tools/osx-cli/lib/src/context.dart @@ -0,0 +1,13 @@ +import 'util/host.dart'; +import 'util/logger.dart'; +import 'util/proc.dart'; + +/// Shared services handed to every command: the logger, detected host facts, +/// and the process runner (which uses the logger for tracing). +class CliContext { + CliContext({required this.log, required this.host}) : proc = Proc(log); + + final Logger log; + final Host host; + final Proc proc; +} diff --git a/tools/osx-cli/lib/src/emulator.dart b/tools/osx-cli/lib/src/emulator.dart new file mode 100644 index 0000000..110f688 --- /dev/null +++ b/tools/osx-cli/lib/src/emulator.dart @@ -0,0 +1,309 @@ +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:path/path.dart' as p; + +import 'util/errors.dart'; +import 'util/host.dart'; +import 'util/logger.dart'; +import 'util/proc.dart'; + +/// A bootable OSCortex image plus how it boots. +class BootImage { + BootImage({required this.path, required this.arch, required this.kind}); + + final String path; + final Arch arch; + + /// `iso` (UEFI cdrom) or `kernel` (direct `-kernel`, aarch64 only). + final String kind; + + bool get isKernel => kind == 'kernel'; +} + +/// Resolves and boots OSCortex images in QEMU, choosing HVF/KVM vs TCG by +/// matching the requested arch against the host arch. +/// +/// QEMU invocations mirror the project's proven configs: +/// * aarch64 `-kernel` (interactive): `~/OSCortex-run/run-arm.sh` +/// -M virt[,accel=hvf] -cpu host/cortex-a72 -smp 1 -m 4G -kernel +/// -device ramfb -device virtio-tablet-device -device virtio-keyboard-device +/// * aarch64 ISO: edk2 AAVMF UEFI (-M virt + pflash) — see build-iso-aarch64.sh. +/// * x86_64 ISO: q35 + OVMF/edk2, -cdrom — see scripts/run-qemu.sh. +class Emulator { + Emulator(this.host, this.log, this.proc); + + final Host host; + final Logger log; + final Proc proc; + + /// Default release tag the bootable ISOs are fetched from when no local image + /// is found. Overridable via `--release`. + static const defaultReleaseTag = 'v0.1.2'; + static const _githubRepo = 'DotCorr/oscortex'; + + /// QEMU share dir (firmware lives here) derived from the qemu binary path. + String? _qemuShareDir(String qemuBin) { + try { + final resolved = File(qemuBin).resolveSymbolicLinksSync(); + // /bin/qemu-system-* -> /share/qemu + final prefix = p.dirname(p.dirname(resolved)); + final share = p.join(prefix, 'share', 'qemu'); + return Directory(share).existsSync() ? share : null; + } catch (_) { + return null; + } + } + + String _qemuBinary(Arch arch) => + arch == Arch.aarch64 ? 'qemu-system-aarch64' : 'qemu-system-x86_64'; + + Future ensureQemu(Arch arch) async { + final bin = _qemuBinary(arch); + if (!await Proc.exists(bin)) { + throw CliException( + 'QEMU for ${arch.label} not found ($bin).', + hint: host.isMacOS + ? 'Install with: brew install qemu' + : 'Install QEMU (e.g. apt install qemu-system).', + ); + } + } + + /// Find a local image for [arch], or download the release ISO. + /// [explicit] (from --image) wins. [releaseTag] sets the fetch tag. + Future resolveImage({ + required Arch arch, + String? explicit, + String? localKernel, + required String releaseTag, + required String cacheDir, + }) async { + if (explicit != null) { + if (!File(explicit).existsSync()) { + throw CliException('Image not found: $explicit'); + } + final kind = explicit.endsWith('.iso') ? 'iso' : 'kernel'; + return BootImage(path: explicit, arch: arch, kind: kind); + } + + // aarch64 boots most reliably via a local .kernel under HVF. + if (arch == Arch.aarch64) { + final candidates = [ + if (localKernel != null) localKernel, + p.join(_home, 'OSCortex-run', 'oscortex-aarch64-release.kernel'), + p.join(_home, 'OSCortex-run', 'oscortex-aarch64.kernel'), + ]; + for (final c in candidates) { + if (File(c).existsSync()) { + log.trace('using local kernel: $c'); + return BootImage(path: c, arch: arch, kind: 'kernel'); + } + } + } + + // Otherwise fetch the release ISO. + final iso = await _fetchReleaseIso(arch, releaseTag, cacheDir); + return BootImage(path: iso, arch: arch, kind: 'iso'); + } + + Future _fetchReleaseIso( + Arch arch, String tag, String cacheDir) async { + final asset = 'oscortex-${arch.label}-$tag.iso'; + final dest = File(p.join(cacheDir, tag, asset)); + if (dest.existsSync() && dest.lengthSync() > 0) { + log.trace('using cached ISO: ${dest.path}'); + return dest.path; + } + dest.parent.createSync(recursive: true); + final url = 'https://github.com/$_githubRepo/releases/download/$tag/$asset'; + log.info('Downloading bootable image: $asset ($tag)'); + await proc.run( + 'curl', + ['-fSL', url, '-o', dest.path], + what: 'curl $asset', + ); + if (!dest.existsSync() || dest.lengthSync() == 0) { + throw CliException('Download produced an empty file: ${dest.path}'); + } + log.ok('Image ready: ${dest.path}'); + return dest.path; + } + + /// Assemble + exec the QEMU command for [image]. Replaces the current process + /// so Ctrl-C / window-close behave like running QEMU directly. + Future boot( + BootImage image, { + required bool headless, + String? serialLog, + int memMiB = 4096, + }) async { + final arch = image.arch; + await ensureQemu(arch); + final qemu = _qemuBinary(arch); + + final accelerate = host.canAccelerate(arch); + final accel = accelerate ? host.accelerator : null; + if (accelerate) { + log.info('Booting ${arch.label} with hardware acceleration ($accel).'); + } else { + log.warn('Booting ${arch.label} under TCG emulation (no $arch accel on ' + '${host.os}/${host.arch?.label}). This is correct but slow.'); + } + + final args = arch == Arch.aarch64 + ? _aarch64Args(image, accel: accel, headless: headless, memMiB: memMiB) + : _x86Args(image, accel: accel, headless: headless, memMiB: memMiB); + + // Serial routing. + if (serialLog != null) { + args.addAll(['-serial', 'file:$serialLog']); + log.detail('serial -> $serialLog'); + } else if (headless) { + args.addAll(['-serial', 'mon:stdio']); + } else { + args.addAll([ + '-serial', + 'file:${p.join(Directory.systemTemp.path, 'oscortex-serial.log')}' + ]); + } + + log.detail('$qemu ${args.join(' ')}'); + log.info(headless + ? 'Headless. Ctrl-A then X to quit.' + : 'Click the window to capture input; release with Ctrl-Alt-G. ' + 'Close the window to quit.'); + + // Hand off: inherit stdio so QEMU's monitor/serial works interactively. + final proc = + await Process.start(qemu, args, mode: ProcessStartMode.inheritStdio); + final code = await proc.exitCode; + if (code != 0) { + throw CliException('QEMU exited with code $code.', exitCode: code); + } + } + + List _aarch64Args( + BootImage image, { + String? accel, + required bool headless, + required int memMiB, + }) { + // HVF requires `-cpu host`; TCG uses a concrete core model. + final machine = accel != null ? 'virt,accel=$accel,gic-version=2' : 'virt'; + final cpu = accel != null ? 'host' : 'cortex-a72'; + + final args = [ + '-M', machine, + '-cpu', cpu, + '-smp', '1', // OSCortex is a cooperative single-core scheduler. + '-m', '${memMiB}M', + // virtio input: -M virt has no PS/2; this is how the shell gets mouse/kbd. + // USB/xHCI hits an HVF assert under QEMU 11, so we use virtio (proven). + '-device', 'ramfb', + '-device', 'virtio-tablet-device', + '-device', 'virtio-keyboard-device', + ]; + + if (image.isKernel) { + args.addAll(['-kernel', image.path]); + } else { + // UEFI ISO boot via edk2 AAVMF (read-only code pflash + writable varstore). + final pflash = _aavmfPflash(); + args.addAll([ + '-drive', + 'if=pflash,format=raw,readonly=on,file=${pflash.code}', + '-drive', + 'if=pflash,format=raw,file=${pflash.vars}', + '-cdrom', + image.path, + ]); + } + + args.addAll(headless ? ['-display', 'none'] : ['-display', _guiDisplay()]); + args.add('-no-reboot'); + return args; + } + + List _x86Args( + BootImage image, { + String? accel, + required bool headless, + required int memMiB, + }) { + final args = [ + '-machine', + accel != null ? 'q35,accel=$accel' : 'q35', + if (accel == null) ...['-cpu', 'qemu64,+x2apic'], + if (accel != null) ...['-cpu', 'host'], + '-smp', + '2', + '-m', + '${memMiB}M', + '-device', + 'qemu-xhci,id=xhci', + ]; + + // x86_64 OSCortex images are UEFI; boot via OVMF/edk2 if available, else + // fall back to a plain -cdrom (QEMU's built-in firmware). + final ovmf = _ovmfCode(); + if (ovmf != null) { + args.addAll(['-drive', 'if=pflash,format=raw,readonly=on,file=$ovmf']); + } + args.addAll(['-cdrom', image.path]); + + args.addAll(headless ? ['-display', 'none'] : ['-display', _guiDisplay()]); + args.add('-no-reboot'); + return args; + } + + String _guiDisplay() => host.isMacOS ? 'cocoa' : 'gtk'; + + ({String code, String vars}) _aavmfPflash() { + final dir = _resolveShareFor('qemu-system-aarch64'); + if (dir == null) { + throw CliException( + 'Could not locate QEMU firmware (edk2-aarch64-code.fd).', + hint: 'Install QEMU with edk2 firmware, or boot a .kernel image.'); + } + final code = p.join(dir, 'edk2-aarch64-code.fd'); + if (!File(code).existsSync()) { + throw CliException('Missing aarch64 UEFI firmware: $code', + hint: 'Reinstall QEMU, or use an aarch64 .kernel image (faster).'); + } + // edk2 needs a writable, 64MiB varstore; create a private copy. + final vars = p.join(Directory.systemTemp.path, 'oscortex-aavmf-vars.fd'); + if (!File(vars).existsSync()) { + final blank = Uint8List(64 * 1024 * 1024); + File(vars).writeAsBytesSync(blank); + } + return (code: code, vars: vars); + } + + String? _ovmfCode() { + final dir = _resolveShareFor('qemu-system-x86_64'); + if (dir == null) return null; + for (final name in ['edk2-x86_64-code.fd', 'OVMF_CODE.fd', 'OVMF.fd']) { + final f = p.join(dir, name); + if (File(f).existsSync()) return f; + } + return null; + } + + String? _resolveShareFor(String qemuBin) { + try { + final probe = Platform.isWindows ? 'where' : 'which'; + final r = Process.runSync(probe, [qemuBin]); + if (r.exitCode != 0) return null; + final bin = (r.stdout as String).trim().split('\n').first; + return _qemuShareDir(bin); + } catch (_) { + return null; + } + } + + String get _home => + Platform.environment['HOME'] ?? + Platform.environment['USERPROFILE'] ?? + '.'; +} diff --git a/tools/osx-cli/lib/src/engine.dart b/tools/osx-cli/lib/src/engine.dart new file mode 100644 index 0000000..aa3852a --- /dev/null +++ b/tools/osx-cli/lib/src/engine.dart @@ -0,0 +1,68 @@ +import 'dart:io'; + +import 'package:path/path.dart' as p; + +import 'util/artifact_config.dart'; +import 'util/errors.dart'; +import 'util/host.dart'; +import 'util/logger.dart'; +import 'util/oscortex_repo.dart'; +import 'util/proc.dart'; + +/// Manages the prebuilt OSCortex engine + gen_snapshot via the repo's +/// `engine-port/fetch-engine.sh`. We do not re-implement the download/checksum +/// logic — we shell out to the canonical script so the pin in +/// `engine-port/artifact.config` stays the single source of truth. +class EngineManager { + EngineManager(this.repo, this.log, this.proc); + + final OscortexRepo repo; + final Logger log; + final Proc proc; + + ArtifactConfig get config => ArtifactConfig.parse(File(repo.artifactConfig)); + + /// Directory where fetch-engine.sh caches a fetched ([arch],release) bundle: + /// `/.engine-cache//oscortex--release`. + Directory cacheDir(Arch arch) { + final version = config.artifactVersion ?? 'oscortex-engine-2'; + return Directory(p.join( + repo.engineCache, + version, + 'oscortex-${arch.engineToken}-release', + )); + } + + File genSnapshot(Arch arch) => + File(p.join(cacheDir(arch).path, 'gen_snapshot')); + + File engineSo(Arch arch) => + File(p.join(cacheDir(arch).path, 'libflutter_engine.so')); + + bool isFetched(Arch arch) => engineSo(arch).existsSync(); + + /// Fetch (or reuse cached) engine + gen_snapshot for [arch]. Idempotent. + Future fetch(Arch arch) async { + if (isFetched(arch)) { + log.trace( + 'engine ${arch.engineToken} already cached at ${cacheDir(arch).path}'); + return; + } + log.info('Fetching prebuilt engine (${arch.label}) ' + 'via engine-port/fetch-engine.sh ...'); + await proc.run( + 'bash', + [repo.fetchEngine, arch.engineToken, 'release'], + workingDirectory: repo.root, + what: 'fetch-engine.sh ${arch.engineToken} release', + ); + if (!isFetched(arch)) { + throw CliException( + 'Engine fetch reported success but ${engineSo(arch).path} is missing.', + hint: 'Check engine-port/artifact.config (ARTIFACT_VERSION / ' + 'ARTIFACT_BASE_URL) and your network.', + ); + } + log.ok('Engine ready: ${arch.label} (${config.artifactVersion})'); + } +} diff --git a/tools/osx-cli/lib/src/osx_bundle.dart b/tools/osx-cli/lib/src/osx_bundle.dart new file mode 100644 index 0000000..aba6a73 --- /dev/null +++ b/tools/osx-cli/lib/src/osx_bundle.dart @@ -0,0 +1,205 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:path/path.dart' as p; + +import 'util/host.dart'; + +/// Writes OSCortex `.osx` bundles. +/// +/// ## Background +/// The kernel's app registry (`kernel/src/app_registry/mod.rs`) reads a v1 +/// bundle: a fixed 112-byte header (`OSCP` magic, `format_version=1`, name, +/// version, entry_offset, stack_size, `aot_len`) followed by exactly one AOT +/// snapshot. It copies `aot_len` bytes into the app record and **ignores +/// everything past `112 + aot_len`**. There is no per-arch selection in v1 — a +/// v1 bundle is implicitly single-arch. +/// +/// ## What this packer produces +/// We want app developers to ship ONE artifact that is native for both +/// aarch64 and x86_64. Two outputs are produced from one build: +/// +/// 1. **Per-arch v1 bundles** (`-arm64.osx`, `-x64.osx`) — each is a +/// plain v1 bundle the current kernel installs and runs unchanged. These are +/// what actually boot today. +/// +/// 2. **A universal (fat) bundle** (`.osx`) — a strict superset: +/// * Bytes `0..112` : a **valid v1 header** whose `aot_len` covers the +/// host-arch snapshot, placed first. An unmodified kernel reads this and +/// loads that arch — so the fat bundle never bricks an old OS. +/// * `112 .. 112+aot_len` : the primary (host-arch) AOT snapshot. +/// * **trailer** (ignored by v1): a `OSXM` multi-arch index listing every +/// arch's (token, offset, length) so an arch-aware loader can pick the +/// right snapshot at install/run time, followed by the non-primary +/// snapshots. +/// +/// The trailer is documented below so the kernel can gain arch-selection later +/// without changing the v1 path. Until then the per-arch bundles are the +/// reliable install targets and the maintainer report flags this. +class OsxBundle { + // v1 header layout (must match oscortex-pack.py + the kernel parser). + static const _magic = [0x4F, 0x53, 0x43, 0x50]; // "OSCP" + static const _formatVersion = 1; + static const headerSize = 112; + + // Trailer magic for the multi-arch index: "OSXM". + static const _trailerMagic = [0x4F, 0x53, 0x58, 0x4D]; + static const _trailerVersion = 1; + + /// Build a single-arch v1 bundle for [snapshot] and write it to [out]. + static void writeSingleArch({ + required Uint8List snapshot, + required String name, + required String version, + required File out, + }) { + final header = _v1Header( + name: name, + version: version, + aotLen: snapshot.length, + ); + out.parent.createSync(recursive: true); + final sink = out.openSync(mode: FileMode.write); + try { + sink.writeFromSync(header); + sink.writeFromSync(snapshot); + } finally { + sink.closeSync(); + } + } + + /// Build the universal (fat) bundle from per-arch [snapshots], with + /// [primary] placed in the v1 slot (so an unmodified kernel boots it). + /// + /// Layout: [v1 header][primary snapshot][OSXM index][other snapshots...]. + static void writeUniversal({ + required Map snapshots, + required Arch primary, + required String name, + required String version, + required File out, + }) { + assert(snapshots.containsKey(primary)); + final primarySnap = snapshots[primary]!; + + // Order: primary first (its bytes live right after the header so the v1 + // header's aot_len describes them), then the rest in a stable order. + final ordered = [ + primary, + ...snapshots.keys.where((a) => a != primary) + ]; + + final builder = BytesBuilder(copy: false); + builder.add( + _v1Header(name: name, version: version, aotLen: primarySnap.length)); + + // Compute offsets as we lay snapshots down. The index records absolute + // file offsets so a loader can seek directly. + final entries = <_ArchEntry>[]; + var cursor = headerSize; + + // Primary snapshot immediately follows the header. + builder.add(primarySnap); + entries.add(_ArchEntry(primary, cursor, primarySnap.length)); + cursor += primarySnap.length; + + // Reserve the index size so we can record post-index offsets correctly: + // the index has a fixed-size head + one fixed record per arch. + final indexLen = _indexLength(ordered.length); + final indexOffset = cursor; + cursor += indexLen; + + // Non-primary snapshots come after the index. + final tail = BytesBuilder(copy: false); + for (final a in ordered.where((a) => a != primary)) { + final snap = snapshots[a]!; + entries.add(_ArchEntry(a, cursor, snap.length)); + tail.add(snap); + cursor += snap.length; + } + + // Now we can build the index (offsets are all known) and append + tail. + builder.add(_buildIndex(entries, indexOffset, indexLen)); + builder.add(tail.toBytes()); + + out.parent.createSync(recursive: true); + out.writeAsBytesSync(builder.toBytes()); + } + + static Uint8List _v1Header({ + required String name, + required String version, + required int aotLen, + int entryOffset = 0, + int stackSize = 0, + }) { + final h = Uint8List(headerSize); + final bd = ByteData.sublistView(h); + h.setRange(0, 4, _magic); + bd.setUint32(4, _formatVersion, Endian.little); + + final nameB = _utf8Clamp(name, 63); + h.setRange(8, 8 + nameB.length, nameB); + final verB = _utf8Clamp(version, 15); + h.setRange(72, 72 + verB.length, verB); + + bd.setUint64(88, entryOffset, Endian.little); + bd.setUint64(96, stackSize, Endian.little); + bd.setUint32(104, aotLen, Endian.little); + bd.setUint32(108, 0, Endian.little); // reserved + return h; + } + + // OSXM index: [magic(4)][ver(u32)][count(u32)][reserved(u32)] then `count` + // records of [token(8, null-padded ascii)][offset(u64)][len(u64)]. + static const _idxHead = 16; + static const _idxRecord = 24; + static int _indexLength(int count) => _idxHead + _idxRecord * count; + + static Uint8List _buildIndex(List<_ArchEntry> entries, int at, int len) { + final b = Uint8List(len); + final bd = ByteData.sublistView(b); + b.setRange(0, 4, _trailerMagic); + bd.setUint32(4, _trailerVersion, Endian.little); + bd.setUint32(8, entries.length, Endian.little); + bd.setUint32(12, 0, Endian.little); + var o = _idxHead; + for (final e in entries) { + final tok = _utf8Clamp(e.arch.osxToken, 8); + b.setRange(o, o + tok.length, tok); + bd.setUint64(o + 8, e.offset, Endian.little); + bd.setUint64(o + 16, e.length, Endian.little); + o += _idxRecord; + } + return b; + } + + static Uint8List _utf8Clamp(String s, int maxBytes) { + final bytes = utf8.encode(s); + return bytes.length <= maxBytes + ? bytes + : Uint8List.sublistView(bytes, 0, maxBytes); + } +} + +/// Output paths for a build: the universal bundle plus one per arch, all in +/// [outDir] and named after [displayName]. +class OsxOutputs { + OsxOutputs(String outDir, String displayName) + : universal = File(p.join(outDir, '$displayName.osx')), + perArch = { + for (final a in Arch.values) + a: File(p.join(outDir, '$displayName-${a.osxToken}.osx')), + }; + + final File universal; + final Map perArch; +} + +class _ArchEntry { + _ArchEntry(this.arch, this.offset, this.length); + final Arch arch; + final int offset; + final int length; +} diff --git a/tools/osx-cli/lib/src/util/app_config.dart b/tools/osx-cli/lib/src/util/app_config.dart new file mode 100644 index 0000000..3b99b14 --- /dev/null +++ b/tools/osx-cli/lib/src/util/app_config.dart @@ -0,0 +1,68 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:path/path.dart' as p; + +/// Per-app OSCortex config, written by `osx init` into `/.osx/config.json` +/// and read by `osx build` / `osx run`. Keeps app-local choices (display name, +/// pinned engine, the OSCortex tree to use) out of the global environment. +class AppConfig { + AppConfig({ + required this.appDir, + this.displayName, + this.oscortexRoot, + this.engineVersion, + this.bundleVersion, + }); + + /// Absolute path of the Flutter app this config belongs to. + final String appDir; + + /// Display name shown in the OSCortex launcher. Defaults to the pubspec name. + String? displayName; + + /// OSCortex source tree recorded at init time (so build/run need no flags). + String? oscortexRoot; + + /// Engine artifact tag pinned for this app (e.g. `oscortex-engine-2`). + String? engineVersion; + + /// Version string baked into the .osx header. Defaults to the pubspec version. + String? bundleVersion; + + static String dirFor(String appDir) => p.join(appDir, '.osx'); + static String pathFor(String appDir) => p.join(dirFor(appDir), 'config.json'); + + File get file => File(pathFor(appDir)); + + /// Load config for [appDir], or null if `osx init` has not been run. + static AppConfig? load(String appDir) { + final f = File(pathFor(appDir)); + if (!f.existsSync()) return null; + final m = jsonDecode(f.readAsStringSync()) as Map; + return AppConfig( + appDir: appDir, + displayName: m['displayName'] as String?, + oscortexRoot: m['oscortexRoot'] as String?, + engineVersion: m['engineVersion'] as String?, + bundleVersion: m['bundleVersion'] as String?, + ); + } + + /// Raw map form, used to seed repo resolution before a full load. + Map get asMap => { + if (oscortexRoot != null) 'oscortexRoot': oscortexRoot!, + if (engineVersion != null) 'engineVersion': engineVersion!, + }; + + void save() { + Directory(dirFor(appDir)).createSync(recursive: true); + const encoder = JsonEncoder.withIndent(' '); + file.writeAsStringSync('${encoder.convert({ + 'displayName': displayName, + 'oscortexRoot': oscortexRoot, + 'engineVersion': engineVersion, + 'bundleVersion': bundleVersion, + })}\n'); + } +} diff --git a/tools/osx-cli/lib/src/util/artifact_config.dart b/tools/osx-cli/lib/src/util/artifact_config.dart new file mode 100644 index 0000000..8cb894c --- /dev/null +++ b/tools/osx-cli/lib/src/util/artifact_config.dart @@ -0,0 +1,51 @@ +import 'dart:io'; + +/// Reads the shell-style key/value pairs out of `engine-port/artifact.config` +/// (the single source of truth for the engine pin + release host) so the CLI +/// never hard-codes the engine tag or repo. Values may reference an env-var +/// default like `${OSCORTEX_ENGINE_BASE_URL:-https://...}`; we resolve those. +class ArtifactConfig { + ArtifactConfig(this._values); + + final Map _values; + + String? get artifactVersion => _values['ARTIFACT_VERSION']; + String? get githubRepo => _values['GITHUB_REPO']; + String? get artifactBaseUrl => _values['ARTIFACT_BASE_URL']; + String? get flutterVersion => _values['FLUTTER_VERSION']; + + static ArtifactConfig parse(File f) { + final values = {}; + if (!f.existsSync()) return ArtifactConfig(values); + + final assign = RegExp(r'^\s*([A-Z0-9_]+)=(.*)$'); + for (final raw in f.readAsLinesSync()) { + final line = raw.trim(); + if (line.isEmpty || line.startsWith('#')) continue; + final m = assign.firstMatch(line); + if (m == null) continue; + values[m.group(1)!] = _resolve(_stripQuotes(m.group(2)!.trim()), values); + } + return ArtifactConfig(values); + } + + static String _stripQuotes(String v) { + // Drop a trailing inline comment that isn't inside quotes (best-effort). + if ((v.startsWith('"') && v.endsWith('"')) || + (v.startsWith("'") && v.endsWith("'"))) { + return v.substring(1, v.length - 1); + } + return v; + } + + /// Expand `${VAR}` / `${VAR:-default}` against the environment, then prior + /// values in the same file. Mirrors how bash `source`s the config. + static String _resolve(String v, Map prior) { + final ref = RegExp(r'\$\{([A-Z0-9_]+)(?::-(.*?))?\}'); + return v.replaceAllMapped(ref, (m) { + final name = m.group(1)!; + final fallback = m.group(2) ?? ''; + return Platform.environment[name] ?? prior[name] ?? fallback; + }); + } +} diff --git a/tools/osx-cli/lib/src/util/errors.dart b/tools/osx-cli/lib/src/util/errors.dart new file mode 100644 index 0000000..d4ec22e --- /dev/null +++ b/tools/osx-cli/lib/src/util/errors.dart @@ -0,0 +1,18 @@ +/// Thrown by commands for an expected, user-facing failure (missing toolchain, +/// bad config, a wrapped script exiting non-zero, …). [main] catches this and +/// prints [message] without a Dart stack trace, then exits with [exitCode]. +/// +/// Use this for "the user did something we can explain" failures. Let real bugs +/// throw normally so their stack traces surface. +class CliException implements Exception { + CliException(this.message, {this.exitCode = 1, this.hint}); + + final String message; + final int exitCode; + + /// Optional actionable next step shown after the error. + final String? hint; + + @override + String toString() => message; +} diff --git a/tools/osx-cli/lib/src/util/host.dart b/tools/osx-cli/lib/src/util/host.dart new file mode 100644 index 0000000..df66041 --- /dev/null +++ b/tools/osx-cli/lib/src/util/host.dart @@ -0,0 +1,77 @@ +import 'dart:io'; + +/// CPU architectures OSCortex targets. +enum Arch { + aarch64, + x86_64; + + /// Short token used in artifact names: `arm64` / `x64`. + /// Matches `engine-port/fetch-engine.sh` and the engine release assets. + String get engineToken => this == Arch.aarch64 ? 'arm64' : 'x64'; + + /// Token used in the .osx per-arch payload index and filenames. + String get osxToken => this == Arch.aarch64 ? 'arm64' : 'x64'; + + /// Human label. + String get label => this == Arch.aarch64 ? 'aarch64' : 'x86_64'; + + static Arch? parse(String s) { + switch (s.toLowerCase()) { + case 'arm64': + case 'aarch64': + return Arch.aarch64; + case 'x64': + case 'x86_64': + case 'amd64': + return Arch.x86_64; + } + return null; + } +} + +/// Facts about the machine the CLI is running on. Used to decide HVF/KVM vs TCG +/// and which gen_snapshot can run natively. +class Host { + Host({required this.os, required this.arch}); + + /// 'macos' | 'linux' | 'windows' | other (`Platform.operatingSystem`). + final String os; + + /// Host CPU architecture, or null if unrecognised. + final Arch? arch; + + static Host detect() { + return Host(os: Platform.operatingSystem, arch: _detectArch()); + } + + bool get isMacOS => os == 'macos'; + bool get isLinux => os == 'linux'; + + /// The hardware accelerator available for QEMU on this host, if any. + /// `hvf` on Apple Silicon/Intel macOS, `kvm` on Linux, else null (TCG only). + String? get accelerator { + if (isMacOS) return 'hvf'; + if (isLinux && File('/dev/kvm').existsSync()) return 'kvm'; + return null; + } + + /// True when [target] can run with hardware acceleration on this host — + /// i.e. the requested arch matches the host arch and an accelerator exists. + bool canAccelerate(Arch target) => arch == target && accelerator != null; + + static Arch? _detectArch() { + // Platform.version embeds the host arch token, e.g. "... on \"macos_arm64\"". + final v = Platform.version; + if (v.contains('arm64') || v.contains('aarch64')) return Arch.aarch64; + if (v.contains('x64') || v.contains('x86_64')) return Arch.x86_64; + + // Fallback: `uname -m` on POSIX. + if (!Platform.isWindows) { + try { + final out = Process.runSync('uname', ['-m']).stdout as String; + return Arch.parse(out.trim()); + } catch (_) {/* ignore */} + } + return null; + } +} diff --git a/tools/osx-cli/lib/src/util/logger.dart b/tools/osx-cli/lib/src/util/logger.dart new file mode 100644 index 0000000..854b83e --- /dev/null +++ b/tools/osx-cli/lib/src/util/logger.dart @@ -0,0 +1,37 @@ +import 'dart:io'; + +/// Minimal ANSI-aware logger. Colours are disabled automatically when stdout is +/// not a terminal (e.g. piped into a file or CI) or when `NO_COLOR` is set. +class Logger { + Logger({bool? verbose}) : verbose = verbose ?? false; + + /// When true, [trace] messages are printed. + bool verbose; + + static final bool _color = + stdout.hasTerminal && Platform.environment['NO_COLOR'] == null; + + static String _wrap(String code, String s) => _color ? '$code$s\x1b[0m' : s; + + /// Informational step, e.g. "fetching engine". + void info(String msg) => stdout.writeln('${_wrap('\x1b[36m', '•')} $msg'); + + /// Success / completion. + void ok(String msg) => stdout.writeln('${_wrap('\x1b[32m', '✓')} $msg'); + + /// Non-fatal warning. + void warn(String msg) => + stderr.writeln('${_wrap('\x1b[33m', '!')} ${_wrap('\x1b[33m', msg)}'); + + /// Fatal error (does not exit — caller decides). + void error(String msg) => + stderr.writeln('${_wrap('\x1b[31m', '✗')} ${_wrap('\x1b[31m', msg)}'); + + /// Verbose-only diagnostic. + void trace(String msg) { + if (verbose) stderr.writeln(_wrap('\x1b[90m', ' $msg')); + } + + /// A dim sub-line under an [info]/[ok] line. + void detail(String msg) => stdout.writeln(_wrap('\x1b[90m', ' $msg')); +} diff --git a/tools/osx-cli/lib/src/util/oscortex_repo.dart b/tools/osx-cli/lib/src/util/oscortex_repo.dart new file mode 100644 index 0000000..a7d6769 --- /dev/null +++ b/tools/osx-cli/lib/src/util/oscortex_repo.dart @@ -0,0 +1,77 @@ +import 'dart:io'; + +import 'package:path/path.dart' as p; + +import 'errors.dart'; + +/// Locates the OSCortex source tree, which holds the build scripts this CLI +/// orchestrates (`tools/build-flutter-osx.sh`, `engine-port/*`, +/// `tools/oscortex-pack.py`, `scripts/run-*.sh`, the bootable images, …). +/// +/// Resolution order: +/// 1. `--oscortex-root` flag (passed in as [override]). +/// 2. `OSCORTEX_ROOT` environment variable. +/// 3. `oscortexRoot` recorded in the app's `.osx/config.json` (from `osx init`). +/// 4. The repo this CLI itself lives in (when run from source at +/// `tools/osx-cli`) — handy for maintainers/contributors. +/// +/// Throws [CliException] with guidance if none resolve to a valid tree. +class OscortexRepo { + OscortexRepo(this.root); + + /// Absolute path to the OSCortex repo root. + final String root; + + // Files we orchestrate. Centralised so a moved script fails clearly. + String get buildFlutterOsx => p.join(root, 'tools', 'build-flutter-osx.sh'); + String get compileAppAot => p.join(root, 'engine-port', 'compile-app-aot.sh'); + String get fetchEngine => p.join(root, 'engine-port', 'fetch-engine.sh'); + String get pack => p.join(root, 'tools', 'oscortex-pack.py'); + String get artifactConfig => p.join(root, 'engine-port', 'artifact.config'); + String get runX86 => p.join(root, 'scripts', 'run-qemu.sh'); + String get runAarch64 => p.join(root, 'scripts', 'run-aarch64.sh'); + String get engineCache => p.join(root, '.engine-cache'); + + /// Validate that this looks like a real OSCortex tree. + bool get looksValid => + File(buildFlutterOsx).existsSync() && File(fetchEngine).existsSync(); + + static OscortexRepo resolve({ + String? override, + Map? appConfig, + }) { + final candidates = [ + override, + Platform.environment['OSCORTEX_ROOT'], + appConfig?['oscortexRoot'], + _selfRepoRoot(), + ]; + + for (final c in candidates) { + if (c == null || c.isEmpty) continue; + final abs = p.normalize(p.absolute(c)); + final repo = OscortexRepo(abs); + if (repo.looksValid) return repo; + } + + throw CliException( + 'Could not find the OSCortex source tree.', + hint: 'Set OSCORTEX_ROOT, pass --oscortex-root , or run `osx init` ' + 'from your app directory to record it.', + ); + } + + /// When the CLI runs from source (`tools/osx-cli`), walk up to the repo root. + static String? _selfRepoRoot() { + // Platform.script points at .../tools/osx-cli/bin/osx.dart when run via + // `dart run`. Walk up looking for the marker scripts. + var dir = p.dirname(Platform.script.toFilePath()); + for (var i = 0; i < 8 && dir != p.dirname(dir); i++) { + if (File(p.join(dir, 'tools', 'build-flutter-osx.sh')).existsSync()) { + return dir; + } + dir = p.dirname(dir); + } + return null; + } +} diff --git a/tools/osx-cli/lib/src/util/proc.dart b/tools/osx-cli/lib/src/util/proc.dart new file mode 100644 index 0000000..f1068a3 --- /dev/null +++ b/tools/osx-cli/lib/src/util/proc.dart @@ -0,0 +1,88 @@ +import 'dart:async'; +import 'dart:io'; + +import 'errors.dart'; +import 'logger.dart'; + +/// Thin wrapper around [Process] that streams child stdout/stderr straight to +/// the terminal and turns a non-zero exit into a [CliException]. This keeps the +/// UX of the wrapped shell scripts intact (their progress prints live) while +/// giving us a single, consistent failure path. +class Proc { + Proc(this.log); + + final Logger log; + + /// Run [exe] with [args]. Streams I/O live unless [captureStdout] is set, in + /// which case stdout is collected and returned instead of printed. + /// + /// Throws [CliException] on a non-zero exit (unless [allowFailure]). + Future run( + String exe, + List args, { + String? workingDirectory, + Map? environment, + bool captureStdout = false, + bool allowFailure = false, + String? what, + }) async { + final label = what ?? '$exe ${args.join(' ')}'; + log.trace('exec: $label' + '${workingDirectory != null ? ' (cwd=$workingDirectory)' : ''}'); + + final Process proc; + try { + proc = await Process.start( + exe, + args, + workingDirectory: workingDirectory, + environment: environment, + // Inherit the parent env so PATH/FLUTTER_ROOT/etc. flow through. + includeParentEnvironment: true, + mode: captureStdout + ? ProcessStartMode.normal + : ProcessStartMode.inheritStdio, + ); + } on ProcessException catch (e) { + throw CliException( + 'Failed to launch `$exe`: ${e.message}', + hint: 'Is it installed and on your PATH?', + ); + } + + final out = StringBuffer(); + if (captureStdout) { + // Drain both streams; mirror stderr so errors are still visible. + final outDone = proc.stdout + .transform(const SystemEncoding().decoder) + .forEach(out.write); + final errDone = proc.stderr + .transform(const SystemEncoding().decoder) + .forEach(stderr.write); + await Future.wait([outDone, errDone]); + } + + final code = await proc.exitCode; + if (code != 0 && !allowFailure) { + throw CliException('`$label` failed (exit $code).', exitCode: code); + } + return ProcResult(code, out.toString()); + } + + /// True if [exe] resolves on PATH. + static Future exists(String exe) async { + final probe = Platform.isWindows ? 'where' : 'which'; + try { + final r = await Process.run(probe, [exe]); + return r.exitCode == 0; + } catch (_) { + return false; + } + } +} + +class ProcResult { + ProcResult(this.exitCode, this.stdout); + final int exitCode; + final String stdout; +} diff --git a/tools/osx-cli/pubspec.yaml b/tools/osx-cli/pubspec.yaml new file mode 100644 index 0000000..f14ec18 --- /dev/null +++ b/tools/osx-cli/pubspec.yaml @@ -0,0 +1,26 @@ +name: oscortex_cli +description: >- + Build and run Flutter apps for OSCortex. Compiles a Flutter project into a + universal .osx bundle (native aarch64 + x86_64 in one artifact) and previews + it in an emulator across architectures. +version: 0.1.0 +homepage: https://github.com/DotCorr/oscortex +repository: https://github.com/DotCorr/oscortex +issue_tracker: https://github.com/DotCorr/oscortex/issues +# Dual-licensed AGPL-3.0-or-later OR commercial; see the repo LICENSE files. +# Public entrypoint is the `osx` executable (see executables below). The package +# name is `oscortex_cli` to be unambiguous on pub.dev. + +environment: + sdk: ^3.5.0 + +executables: + osx: osx + +dependencies: + args: ^2.5.0 + path: ^1.9.0 + +dev_dependencies: + lints: ^4.0.0 + test: ^1.25.0 diff --git a/tools/osx-cli/test/osx_bundle_test.dart b/tools/osx-cli/test/osx_bundle_test.dart new file mode 100644 index 0000000..54d7189 --- /dev/null +++ b/tools/osx-cli/test/osx_bundle_test.dart @@ -0,0 +1,153 @@ +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:oscortex_cli/src/osx_bundle.dart'; +import 'package:oscortex_cli/src/util/host.dart'; +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; + +/// Reads the v1 header fields the kernel parser (app_registry/mod.rs) reads. +({String magic, int version, String name, String ver, int aotLen}) _readHeader( + Uint8List b) { + final bd = ByteData.sublistView(b); + String cstr(int start, int len) { + final slice = b.sublist(start, start + len); + final end = slice.indexOf(0); + return String.fromCharCodes(slice.sublist(0, end == -1 ? len : end)); + } + + return ( + magic: String.fromCharCodes(b.sublist(0, 4)), + version: bd.getUint32(4, Endian.little), + name: cstr(8, 64), + ver: cstr(72, 16), + aotLen: bd.getUint32(104, Endian.little), + ); +} + +void main() { + Uint8List fakeSnap(int len, int fill) => + Uint8List.fromList(List.filled(len, fill)); + + group('single-arch v1 bundle', () { + test('header matches the kernel/Python v1 layout', () { + final tmp = Directory.systemTemp.createTempSync('osx_test'); + final out = File(p.join(tmp.path, 'App.osx')); + final snap = fakeSnap(2048, 0xAB); + + OsxBundle.writeSingleArch( + snapshot: snap, + name: 'Demo', + version: '1.2.3', + out: out, + ); + + final bytes = out.readAsBytesSync(); + final h = _readHeader(bytes); + + expect(h.magic, 'OSCP'); + expect(h.version, 1); + expect(h.name, 'Demo'); + expect(h.ver, '1.2.3'); + expect(h.aotLen, 2048); + // Payload starts at byte 112 and equals the snapshot. + expect(bytes.length, OsxBundle.headerSize + 2048); + expect(bytes.sublist(OsxBundle.headerSize), snap); + + tmp.deleteSync(recursive: true); + }); + }); + + group('universal (fat) bundle', () { + test('is v1-readable: header describes the primary arch snapshot', () { + final tmp = Directory.systemTemp.createTempSync('osx_test'); + final out = File(p.join(tmp.path, 'App.osx')); + final arm = fakeSnap(1000, 0x11); + final x64 = fakeSnap(1500, 0x22); + + OsxBundle.writeUniversal( + snapshots: {Arch.aarch64: arm, Arch.x86_64: x64}, + primary: Arch.aarch64, + name: 'Demo', + version: '0.1.0', + out: out, + ); + + final bytes = out.readAsBytesSync(); + final h = _readHeader(bytes); + + // An UNMODIFIED kernel reads the v1 header + the first aot_len bytes. + expect(h.magic, 'OSCP'); + expect(h.version, 1); + expect(h.aotLen, arm.length, reason: 'primary snapshot in the v1 slot'); + expect( + bytes.sublist(OsxBundle.headerSize, OsxBundle.headerSize + arm.length), + arm, + reason: 'v1 loader gets the primary arch verbatim', + ); + }); + + test('OSXM index lists both arches with recoverable snapshots', () { + final tmp = Directory.systemTemp.createTempSync('osx_test'); + final out = File(p.join(tmp.path, 'App.osx')); + final arm = fakeSnap(1000, 0x11); + final x64 = fakeSnap(1500, 0x22); + + OsxBundle.writeUniversal( + snapshots: {Arch.aarch64: arm, Arch.x86_64: x64}, + primary: Arch.x86_64, + name: 'Demo', + version: '0.1.0', + out: out, + ); + + final bytes = out.readAsBytesSync(); + final entries = _parseIndex(bytes); + + expect(entries.keys, containsAll(['arm64', 'x64'])); + + // Each recorded (offset,len) must point at the right snapshot bytes. + final armRec = entries['arm64']!; + final x64Rec = entries['x64']!; + expect(bytes.sublist(armRec.$1, armRec.$1 + armRec.$2), arm); + expect(bytes.sublist(x64Rec.$1, x64Rec.$1 + x64Rec.$2), x64); + + // The x64 (primary) snapshot must also be the v1 payload. + expect(x64Rec.$1, OsxBundle.headerSize); + + tmp.deleteSync(recursive: true); + }); + }); +} + +/// Scan for the OSXM index and decode its records. +Map _parseIndex(Uint8List b) { + // Locate "OSXM" after the header. + final magic = [0x4F, 0x53, 0x58, 0x4D]; + var idx = -1; + for (var i = OsxBundle.headerSize; i < b.length - 4; i++) { + if (b[i] == magic[0] && + b[i + 1] == magic[1] && + b[i + 2] == magic[2] && + b[i + 3] == magic[3]) { + idx = i; + break; + } + } + expect(idx, isNot(-1), reason: 'OSXM index present'); + + final bd = ByteData.sublistView(b); + final count = bd.getUint32(idx + 8, Endian.little); + final out = {}; + var o = idx + 16; + for (var i = 0; i < count; i++) { + final tokBytes = b.sublist(o, o + 8); + final end = tokBytes.indexOf(0); + final tok = String.fromCharCodes(tokBytes.sublist(0, end == -1 ? 8 : end)); + final off = bd.getUint64(o + 8, Endian.little); + final len = bd.getUint64(o + 16, Endian.little); + out[tok] = (off, len); + o += 24; + } + return out; +}