diff --git a/docs/contributing/advanced-formatting.mdx b/docs/contributing/advanced-formatting.mdx new file mode 100644 index 00000000..5a471a7b --- /dev/null +++ b/docs/contributing/advanced-formatting.mdx @@ -0,0 +1,794 @@ +--- +sidebar_position: 3 +--- + +# Advanced Formatting Guidelines + +We will go through advanced formatting situations, architectural patterns, and complex Swift/Objective-C features for the SideStore project. + +## Advanced Swift Patterns + +### Protocol-Oriented Programming + +#### Protocol Definitions +- Keep protocols focused and cohesive +- Use associated types for generic protocols +- Provide default implementations in extensions when appropriate + +```swift +// ✅ Good +protocol AppInstalling { + associatedtype AppType: App + + func install(_ app: AppType) async throws + func uninstall(_ app: AppType) async throws +} + +extension AppInstalling { + func validateApp(_ app: AppType) -> Bool { + return !app.identifier.isEmpty && app.version.isValid + } +} + +// ❌ Bad +protocol AppManager { + func install(_ app: Any) -> Bool + func uninstall(_ app: Any) -> Bool + func update(_ app: Any) -> Bool + func backup(_ app: Any) -> Bool + func restore(_ app: Any) -> Bool + // Too many responsibilities +} +``` + +#### Protocol Composition +- Use protocol composition for complex requirements +- Keep individual protocols small and focused + +```swift +// ✅ Good +protocol Downloadable { + var downloadURL: URL { get } + func download() async throws -> Data +} + +protocol Installable { + var installationRequirements: InstallationRequirements { get } + func install() async throws +} + +typealias DeployableApp = App & Downloadable & Installable + +class AppDeployer { + func deploy(_ app: T) async throws { + let data = try await app.download() + try await app.install() + } +} +``` + +### Generics and Type Constraints + +#### Generic Type Definitions +- Use meaningful constraint names +- Prefer protocol constraints over class constraints +- Use `where` clauses for complex constraints + +```swift +// ✅ Good +struct Repository { + private var entities: [Entity.ID: Entity] = [:] + + func save(_ entity: T) where T == Entity { + entities[entity.id] = entity + } + + func find(byId id: ID) -> Entity? where ID == Entity.ID { + return entities[id] + } +} + +// ❌ Bad +struct Repository { + private var items: [String: T] = [:] + // No type safety +} +``` + +#### Advanced Generic Constraints +- Use conditional conformance appropriately +- Leverage phantom types when needed + +```swift +// ✅ Good +extension Array: AppCollection where Element: App { + var installedApps: [Element] { + return filter { $0.isInstalled } + } + + func sortedByInstallDate() -> [Element] { + return sorted { $0.installDate < $1.installDate } + } +} + +// Phantom types for type safety +struct AppState { + let app: App +} + +enum Downloaded {} +enum Installed {} + +typealias DownloadedApp = AppState +typealias InstalledApp = AppState +``` + +### Async/Await Patterns + +#### Async Function Design +- Use async/await consistently +- Structure concurrent operations clearly +- Handle cancellation appropriately + +```swift +// ✅ Good +actor AppInstallationManager { + private var activeInstallations: [String: Task] = [:] + + func installApp(_ app: App) async throws { + // Prevent duplicate installations + if activeInstallations[app.identifier] != nil { + throw InstallationError.alreadyInstalling + } + + let task = Task { + try await performInstallation(app) + } + + activeInstallations[app.identifier] = task + + defer { + activeInstallations.removeValue(forKey: app.identifier) + } + + try await task.value + } + + private func performInstallation(_ app: App) async throws { + // Check for cancellation at key points + try Task.checkCancellation() + + let data = try await downloadApp(app) + + try Task.checkCancellation() + + try await installData(data, for: app) + } +} + +// ❌ Bad +func installApp(_ app: App, completion: @escaping (Error?) -> Void) { + DispatchQueue.global().async { + // Mixing old completion handler style with new async code + let result = await self.downloadApp(app) + DispatchQueue.main.async { + completion(nil) + } + } +} +``` + +#### Structured Concurrency +- Use task groups for related concurrent operations +- Prefer structured concurrency over unstructured tasks + +```swift +// ✅ Good +func installMultipleApps(_ apps: [App]) async throws { + try await withThrowingTaskGroup(of: Void.self) { group in + for app in apps { + group.addTask { + try await self.installApp(app) + } + } + + // Wait for all installations to complete + try await group.waitForAll() + } +} + +// For independent results +func downloadMultipleApps(_ apps: [App]) async throws -> [App: Data] { + try await withThrowingTaskGroup(of: (App, Data).self) { group in + for app in apps { + group.addTask { + let data = try await self.downloadApp(app) + return (app, data) + } + } + + var results: [App: Data] = [:] + for try await (app, data) in group { + results[app] = data + } + return results + } +} +``` + +### Result Builders and DSLs + +#### Custom Result Builders +- Create focused, single-purpose result builders +- Provide clear syntax for domain-specific operations + +```swift +// ✅ Good +@resultBuilder +struct AppConfigurationBuilder { + static func buildBlock(_ components: AppConfigurationComponent...) -> AppConfiguration { + return AppConfiguration(components: components) + } + + static func buildOptional(_ component: AppConfigurationComponent?) -> AppConfigurationComponent? { + return component + } + + static func buildEither(first component: AppConfigurationComponent) -> AppConfigurationComponent { + return component + } + + static func buildEither(second component: AppConfigurationComponent) -> AppConfigurationComponent { + return component + } +} + +// Usage +func configureApp(@AppConfigurationBuilder builder: () -> AppConfiguration) -> App { + let config = builder() + return App(configuration: config) +} + +let app = configureApp { + AppName("SideStore") + AppVersion("1.0.0") + if debugMode { + DebugSettings() + } + Permissions { + NetworkAccess() + FileSystemAccess() + } +} +``` + +## Advanced Objective-C Patterns + +### Category Organization +- Use categories to organize related functionality +- Keep category names descriptive and specific + +```objc +// ✅ Good +@interface NSString (SSValidation) +- (BOOL)ss_isValidAppIdentifier; +- (BOOL)ss_isValidVersion; +@end + +@interface UIViewController (SSAppInstallation) +- (void)ss_presentAppInstallationViewController:(SSApp *)app; +- (void)ss_showInstallationProgress:(SSInstallationProgress *)progress; +@end + +// ❌ Bad +@interface NSString (Helpers) +- (BOOL)isValid; // Too generic +- (NSString *)cleanup; // Unclear purpose +@end +``` + +### Advanced Memory Management +- Use proper patterns for delegate relationships +- Handle complex object graphs correctly + +```objc +// ✅ Good +@interface SSAppInstaller : NSObject +@property (nonatomic, weak) id delegate; +@property (nonatomic, strong) NSOperationQueue *installationQueue; +@end + +@implementation SSAppInstaller + +- (instancetype)init { + self = [super init]; + if (self) { + _installationQueue = [[NSOperationQueue alloc] init]; + _installationQueue.maxConcurrentOperationCount = 3; + _installationQueue.name = @"com.sidestore.installation"; + } + return self; +} + +- (void)installApp:(SSApp *)app completion:(void (^)(NSError *))completion { + __weak typeof(self) weakSelf = self; + NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{ + __strong typeof(weakSelf) strongSelf = weakSelf; + if (!strongSelf) return; + + NSError *error = nil; + [strongSelf performInstallationForApp:app error:&error]; + + dispatch_async(dispatch_get_main_queue(), ^{ + completion(error); + }); + }]; + + [self.installationQueue addOperation:operation]; +} + +@end +``` + +### Block Usage Patterns +- Use proper block patterns for asynchronous operations +- Handle memory management in blocks correctly + +```objc +// ✅ Good +typedef void (^SSInstallationProgressBlock)(float progress); +typedef void (^SSInstallationCompletionBlock)(SSApp *app, NSError *error); + +@interface SSAppDownloader : NSObject +- (NSURLSessionTask *)downloadApp:(SSApp *)app + progress:(SSInstallationProgressBlock)progressBlock + completion:(SSInstallationCompletionBlock)completion; +@end + +@implementation SSAppDownloader + +- (NSURLSessionTask *)downloadApp:(SSApp *)app + progress:(SSInstallationProgressBlock)progressBlock + completion:(SSInstallationCompletionBlock)completion { + + NSURLRequest *request = [NSURLRequest requestWithURL:app.downloadURL]; + + __weak typeof(self) weakSelf = self; + NSURLSessionDownloadTask *task = [[NSURLSession sharedSession] + downloadTaskWithRequest:request + completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) { + __strong typeof(weakSelf) strongSelf = weakSelf; + if (!strongSelf) return; + + if (error) { + dispatch_async(dispatch_get_main_queue(), ^{ + completion(nil, error); + }); + return; + } + + // Process downloaded file + [strongSelf processDownloadedFile:location + forApp:app + completion:completion]; + }]; + + [task resume]; + return task; +} + +@end +``` + +## Architectural Patterns + +### MVVM Implementation +- Separate concerns clearly between Model, View, and ViewModel +- Use proper data binding patterns + +```swift +// ✅ Good +// Model +struct App { + let identifier: String + let name: String + let version: String + let isInstalled: Bool +} + +// ViewModel +@MainActor +class AppListViewModel: ObservableObject { + @Published var apps: [App] = [] + @Published var isLoading = false + @Published var errorMessage: String? + + private let appService: AppService + + init(appService: AppService) { + self.appService = appService + } + + func loadApps() async { + isLoading = true + errorMessage = nil + + do { + apps = try await appService.fetchAvailableApps() + } catch { + errorMessage = error.localizedDescription + } + + isLoading = false + } + + func installApp(_ app: App) async { + do { + try await appService.installApp(app) + await loadApps() // Refresh the list + } catch { + errorMessage = "Failed to install \(app.name): \(error.localizedDescription)" + } + } +} + +// View +struct AppListView: View { + @StateObject private var viewModel: AppListViewModel + + init(appService: AppService) { + _viewModel = StateObject(wrappedValue: AppListViewModel(appService: appService)) + } + + var body: some View { + NavigationView { + List(viewModel.apps, id: \.identifier) { app in + AppRowView(app: app) { + Task { + await viewModel.installApp(app) + } + } + } + .navigationTitle("Available Apps") + .overlay { + if viewModel.isLoading { + ProgressView() + } + } + .alert("Error", isPresented: .constant(viewModel.errorMessage != nil)) { + Button("OK") { + viewModel.errorMessage = nil + } + } message: { + Text(viewModel.errorMessage ?? "") + } + } + .task { + await viewModel.loadApps() + } + } +} +``` + +### Dependency Injection +- Use dependency injection for testability and flexibility +- Consider using a DI container for complex applications + +```swift +// ✅ Good +protocol AppService { + func fetchAvailableApps() async throws -> [App] + func installApp(_ app: App) async throws +} + +protocol NetworkService { + func downloadData(from url: URL) async throws -> Data +} + +class DefaultAppService: AppService { + private let networkService: NetworkService + private let deviceService: DeviceService + + init(networkService: NetworkService, deviceService: DeviceService) { + self.networkService = networkService + self.deviceService = deviceService + } + + func fetchAvailableApps() async throws -> [App] { + let data = try await networkService.downloadData(from: appsURL) + return try JSONDecoder().decode([App].self, from: data) + } + + func installApp(_ app: App) async throws { + guard deviceService.hasSpace(for: app) else { + throw InstallationError.insufficientStorage + } + + let appData = try await networkService.downloadData(from: app.downloadURL) + try await deviceService.installApp(data: appData) + } +} + +// DI Container +class ServiceContainer { + static let shared = ServiceContainer() + + private init() {} + + lazy var networkService: NetworkService = DefaultNetworkService() + lazy var deviceService: DeviceService = DefaultDeviceService() + lazy var appService: AppService = DefaultAppService( + networkService: networkService, + deviceService: deviceService + ) +} +``` + +### Error Handling Architecture +- Create comprehensive error handling strategies +- Use typed errors for better error handling + +```swift +// ✅ Good +enum SideStoreError: Error { + case network(NetworkError) + case installation(InstallationError) + case device(DeviceError) + case validation(ValidationError) +} + +enum NetworkError: Error { + case noConnection + case timeout + case serverError(Int) + case invalidResponse +} + +enum InstallationError: Error { + case insufficientStorage + case incompatibleDevice + case corruptedFile + case alreadyInstalled +} + +extension SideStoreError: LocalizedError { + var errorDescription: String? { + switch self { + case .network(let networkError): + return "Network error: \(networkError.localizedDescription)" + case .installation(let installError): + return "Installation error: \(installError.localizedDescription)" + case .device(let deviceError): + return "Device error: \(deviceError.localizedDescription)" + case .validation(let validationError): + return "Validation error: \(validationError.localizedDescription)" + } + } +} + +// Error handling in services +class AppService { + func installApp(_ app: App) async throws { + do { + try validateApp(app) + } catch { + throw SideStoreError.validation(error as! ValidationError) + } + + do { + try await performInstallation(app) + } catch let error as NetworkError { + throw SideStoreError.network(error) + } catch let error as InstallationError { + throw SideStoreError.installation(error) + } + } +} +``` + +## Performance Considerations + +### Memory Optimization +- Use lazy loading for expensive resources +- Implement proper caching strategies + +```swift +// ✅ Good +class AppImageCache { + private let cache = NSCache() + private let downloadQueue = DispatchQueue(label: "image-download", qos: .utility) + + init() { + cache.countLimit = 50 + cache.totalCostLimit = 50 * 1024 * 1024 // 50MB + } + + func image(for app: App) async -> UIImage? { + let key = app.identifier as NSString + + // Check cache first + if let cachedImage = cache.object(forKey: key) { + return cachedImage + } + + // Download if not cached + return await withCheckedContinuation { continuation in + downloadQueue.async { [weak self] in + guard let self = self else { + continuation.resume(returning: nil) + return + } + + do { + let data = try Data(contentsOf: app.iconURL) + let image = UIImage(data: data) + + if let image = image { + self.cache.setObject(image, forKey: key) + } + + continuation.resume(returning: image) + } catch { + continuation.resume(returning: nil) + } + } + } + } +} +``` + +### Threading Best Practices +- Use appropriate queue priorities +- Minimize context switching + +```swift +// ✅ Good +actor BackgroundProcessor { + private let processingQueue = DispatchQueue( + label: "background-processing", + qos: .utility, + attributes: .concurrent + ) + + func processLargeDataSet(_ data: [LargeDataItem]) async -> [ProcessedItem] { + return await withTaskGroup(of: ProcessedItem?.self, returning: [ProcessedItem].self) { group in + let chunkSize = max(data.count / ProcessInfo.processInfo.activeProcessorCount, 1) + + for chunk in data.chunked(into: chunkSize) { + group.addTask { + return await self.processChunk(chunk) + } + } + + var results: [ProcessedItem] = [] + for await result in group { + if let processed = result { + results.append(processed) + } + } + + return results + } + } + + private func processChunk(_ chunk: [LargeDataItem]) async -> ProcessedItem? { + // CPU-intensive processing + return await withCheckedContinuation { continuation in + processingQueue.async { + let result = chunk.map { self.expensiveOperation($0) } + continuation.resume(returning: ProcessedItem(results: result)) + } + } + } +} +``` + +## Testing Patterns + +### Protocol-Based Testing +- Use protocols for dependency injection in tests +- Create focused test doubles + +```swift +// ✅ Good +class MockAppService: AppService { + var shouldFailInstallation = false + var installedApps: [App] = [] + + func fetchAvailableApps() async throws -> [App] { + return [ + App(identifier: "test.app1", name: "Test App 1", version: "1.0.0"), + App(identifier: "test.app2", name: "Test App 2", version: "2.0.0") + ] + } + + func installApp(_ app: App) async throws { + if shouldFailInstallation { + throw InstallationError.insufficientStorage + } + installedApps.append(app) + } +} + +class AppListViewModelTests: XCTestCase { + private var mockAppService: MockAppService! + private var viewModel: AppListViewModel! + + override func setUp() { + super.setUp() + mockAppService = MockAppService() + viewModel = AppListViewModel(appService: mockAppService) + } + + @MainActor + func testLoadAppsSuccess() async { + await viewModel.loadApps() + + XCTAssertEqual(viewModel.apps.count, 2) + XCTAssertFalse(viewModel.isLoading) + XCTAssertNil(viewModel.errorMessage) + } + + @MainActor + func testInstallAppFailure() async { + mockAppService.shouldFailInstallation = true + + let testApp = App(identifier: "test", name: "Test", version: "1.0") + await viewModel.installApp(testApp) + + XCTAssertNotNil(viewModel.errorMessage) + XCTAssertTrue(viewModel.errorMessage!.contains("insufficient storage")) + } +} +``` + +## Documentation Standards + +### Complex API Documentation +- Document complex behaviors and edge cases +- Provide usage examples for non-trivial APIs + +```swift +/** + * A thread-safe manager for handling app installations with automatic retry logic. + * + * This class manages the installation process for iOS applications, handling + * network downloads, signature verification, and device communication. + * It provides automatic retry functionality for transient failures and + * comprehensive error reporting. + * + * ## Usage Example + * ```swift + * let installer = AppInstallationManager() + * + * do { + * let result = try await installer.installApp( + * from: app.sourceURL, + * identifier: app.bundleID, + * maxRetries: 3 + * ) + * print("Installation completed: \(result.installedPath)") + * } catch InstallationError.insufficientStorage { + * // Handle storage error + * } catch { + * // Handle other errors + * } + * ``` + * + * ## Thread Safety + * This class is thread-safe and can be called from any queue. All completion + * handlers are called on the main queue unless otherwise specified. + * + * ## Error Handling + * Installation failures are categorized into recoverable and non-recoverable + * errors. Recoverable errors (network timeouts, temporary device issues) will + * be automatically retried up to the specified limit. Non-recoverable errors + * (invalid signatures, incompatible devices) will fail immediately. + */ +@MainActor +class AppInstallationManager { + // Implementation +} +``` + +Remember: These advanced patterns should be used judiciously. Always prioritize code clarity and maintainability over clever implementations. When in doubt, choose the simpler approach that still meets the requirements. \ No newline at end of file diff --git a/docs/contributing/contributing.mdx b/docs/contributing/contributing.mdx new file mode 100644 index 00000000..5e08af93 --- /dev/null +++ b/docs/contributing/contributing.mdx @@ -0,0 +1,239 @@ +--- +sidebar_position: 1 +--- + +# Contributing to SideStore + +Thank you for your interest in contributing to SideStore! SideStore is a community driven project, and it's made possible by people like you. + +By contributing to this Project (SideStore), you agree to the Developer's Certificate of Origin found in [CERTIFICATE-OF-ORIGIN.md](https://github.com/SideStore/SideStore/blob/develop/CERTIFICATE-OF-ORIGIN.md). Any contributions to this project after the addition of the Developer's Certificate of Origin are subject to its policy. + +There are many ways to contribute to SideStore, so if you aren't a developer, there are still many other ways you can help out: + +- [Writing documentation](https://github.com/SideStore/SideStore-Docs) +- [Submitting detailed bug reports and suggesting new features](https://github.com/SideStore/SideStore/issues/new/choose) +- Helping out with support: + - [Discord](https://discord.gg/sidestore-949183273383395328) + - [GitHub Discussions](https://github.com/SideStore/SideStore/discussions) + +However, this guide will focus on the development side of things. For now, we will only have setup information here, but you can [join our Discord](https://discord.gg/sidestore-949183273383395328) if you need help after setup. + +## Requirements + +This guide assumes you: + +- are on a Mac +- have Xcode installed +- have basic command line knowledge (know how to run commands, cd into a directory) +- have basic Git knowledge ([GitHub Desktop](https://desktop.github.com/) is a great tool for beginners, and greatly simplifies working with Git) +- have basic Swift/iOS development knowledge + +## Setup + +1. **Fork the SideStore repo on GitHub**. + +2. **Clone the fork**: + ```bash + git clone https://github.com//SideStore.git --recurse + cd SideStore + ``` + + If you are using GitHub Desktop, refer to [this guide](https://docs.github.com/en/desktop/contributing-and-collaborating-using-github-desktop/adding-and-cloning-repositories/cloning-and-forking-repositories-from-github-desktop). + +3. **Copy `CodeSigning.xcconfig.sample` to `CodeSigning.xcconfig`** and fill in the values. + +4. **(Development only)** Change the value for `ALTDeviceID` in the Info.plist to your device's UDID. Normally, SideServer embeds the device's UDID in SideStore's Info.plist during installation. When running through Xcode you'll need to set the value yourself or else SideStore won't resign (or even install) apps for the proper device. + +5. **Finally, open `AltStore.xcworkspace` in Xcode**. + +Next, make and test your changes. Then, commit and push your changes using git and make a pull request. + +## Prebuilt Binary Information + +minimuxer and em_proxy use prebuilt static library binaries built by GitHub Actions to speed up builds and remove the need for Rust to be installed when working on SideStore. [`SideStore/fetch-prebuilt.sh`](https://github.com/SideStore/SideStore/blob/develop/SideStore/fetch-prebuilt.sh) will be run before each build by Xcode, and it will check if the downloaded binaries are up-to-date once every 6 hours. If you want to force it to check for new binaries, run `bash ./SideStore/fetch-prebuilt.sh force`. + +## Building with Xcode + +Install cocoapods if required using: `brew install cocoapods` +Now using commandline on the repository workspace root, perform Pod-Install using: `pod install` command to install the cocoapod dependencies. +After this you can do regular builds within Xcode. + +## Building an IPA for Distribution + +Install cocoapods if required using: `brew install cocoapods` +Now using commandline on the repository workspace root, perform Pod-Install using: `pod install` command to install the cocoapod dependencies. + +You can then use the Makefile command: `make build fakesign ipa` in the root directory. +By default the config for build is: `Release` +For debug builds: `export BUILD_CONFIG=Debug;make build fakesign ipa` in the root directory. +For alpha/beta builds: `export IS_ALPHA=1;` or `export IS_BETA=1;` before invoking the build command. +This will create SideStore.ipa. + +```sh +Examples: + + # cocoapods + brew install cocoapods + # perform installation of the pods + pod install + + # alpha release build + export IS_ALPHA=1;make build fakesign ipa + # alpha debug build + export IS_ALPHA=1;export BUILD_CONFIG=Debug;make build fakesign ipa + + # beta release build + export IS_BETA=1;make build fakesign ipa + # beta debug build + export IS_BETA=1;export BUILD_CONFIG=Debug;make build fakesign ipa + + # stable release build + make build fakesign ipa + # stable debug build + export BUILD_CONFIG=Debug;make build fakesign ipa +``` + +By default sidestore will build for its default bundleIdentifier `com.SideStore.SideStore` but if you need to set a custom bundleID for commandline builds, once can do so by exporting `BUNDLE_ID_SUFFIX` env var: + +```sh + # stable release build + export BUNDLE_ID_SUFFIX=XYZ0123456;make build fakesign ipa + # stable debug build + export BUNDLE_ID_SUFFIX=XYZ0123456;export BUILD_CONFIG=Debug;make build fakesign ipa +``` + +**NOTE**: When building from XCode, the `BUNDLE_ID_SUFFIX` is set by default with the value of `DEVELOPMENT_TEAM` + +This can be customized by setting/removing the BUNDLE_ID_SUFFIX in overriding CodeSigning.xcconfig created from CodeSigning.xcconfig.sample + +:::warning +The binary created will contain paths to Xcode's DerivedData, and if you built minimuxer on your machine, paths to $HOME/.cargo. This will include your username. If you want to keep your user's username private, you might want to get GitHub Actions to build the IPA instead. +::: + +## Developing minimuxer alongside SideStore + +Please see [minimuxer's README](https://github.com/SideStore/minimuxer) for development instructions. + +## Pull Request Process + +### Before Submitting + +1. **Follow coding standards**: Ensure your code follows our [formatting guidelines](./formatting.mdx). + +2. **Test your changes**: + - Test on different iOS versions when possible + - Ensure existing functionality isn't broken + - Add tests for new features when applicable + +3. **Update documentation**: Update relevant documentation if your changes affect user-facing features. + +### Submitting Your PR + +1. **Create a feature branch**: + ```bash + git checkout -b feature/your-feature-name + ``` + +2. **Make your changes**: Follow the coding guidelines and best practices. + +3. **Commit your changes with sign-off**: + ```bash + git add . + git commit -s -m "Add descriptive commit message" + ``` + + **Important**: All commits should be signed off with the Developer Certificate of Origin (DCO). The `-s` flag automatically adds the required `Signed-off-by` line to your commit message. + +4. **Push to your fork**: + ```bash + git push origin feature/your-feature-name + ``` + +5. **Create a Pull Request**: Go to the main SideStore repository and create a pull request from your branch. + +:::caution +We do **not** accept pull requests that are obviously AI-generated or "vibe-coded" (e.g., too many obvious comments, code that looks generic, lacks understanding of the project, or is not thoughtfully made for SideStore). Please make sure your contributions are original, and show clear understanding of the codebase and its goals. +::: + +### PR Guidelines + +- **Clear title**: Use a descriptive title that summarizes the change +- **Detailed description**: Explain what changes you made and why +- **Link related issues**: Reference any related GitHub issues +- **Screenshots**: Include screenshots for UI changes +- **Testing notes**: Describe how you tested your changes + +## Developer Certificate of Origin (DCO) + +SideStore requires all contributors to sign off on their commits using the Developer Certificate of Origin (DCO). This serves as our Contributor Code of Conduct and is a simple way to certify that you wrote or have the right to submit the code you are contributing. + +### What is the DCO? + +By signing off on your commits, you certify that: + +- **(a)** The contribution was created in whole or in part by you and you have the right to submit it under the open source license indicated in the file; or +- **(b)** The contribution is based upon previous work that, to the best of your knowledge, is covered under an appropriate open source license and you have the right under that license to submit that work with modifications; or +- **(c)** The contribution was provided directly to you by some other person who certified (a), (b) or (c) and you have not modified it. +- **(d)** You understand and agree that this project and the contribution are public and that a record of the contribution is maintained indefinitely. + +### How to Sign Off + +To sign off on your commits, simply add the `-s` flag when committing: + +```bash +git commit -s -m "Your commit message" +``` + +This will automatically add a `Signed-off-by` line to your commit message: + +``` +Your commit message + +Signed-off-by: SternXD +``` + +### Important Notes + +- **All commits should be signed off** - We usually prefer that you sign your commits. +- **Use the correct email** - The email should match your GitHub account +- **Retroactive sign-off** - If you forget to sign off, you can amend your last commit with `git commit --amend -s` + +For more details, see the full [Certificate of Origin](https://github.com/SideStore/SideStore/blob/develop/CERTIFICATE-OF-ORIGIN.md) document. + +## Code Review Process + +1. **Initial review**: A maintainer will review your PR within a few days +2. **Feedback**: Address any requested changes or questions +3. **Final approval**: Once approved, a maintainer will merge your PR + +## Guidelines for Good Contributions + +### Code Quality +- Write clean, readable code +- Follow Swift and Objective-C best practices +- Use meaningful variable and function names +- Add comments for code that's complex + +### Testing +- Test on physical devices when possible +- Test edge cases and error conditions +- Verify backward compatibility + +### Documentation +- Update code comments +- Update user-facing documentation +- Include inline documentation for public APIs + +## Getting Help + +If you need help or have questions: + +- **GitHub Discussions**: Use GitHub Discussions for general questions +- **GitHub Issues**: Create an issue for bugs or feature requests +- **Discord**: Join the SideStore Discord community + +## Recognition + +Contributors who make significant contributions will be recognized in the project's credits and changelog. + +Thank you for contributing to SideStore! \ No newline at end of file diff --git a/docs/contributing/formatting.mdx b/docs/contributing/formatting.mdx new file mode 100644 index 00000000..e8681ba3 --- /dev/null +++ b/docs/contributing/formatting.mdx @@ -0,0 +1,492 @@ +--- +sidebar_position: 2 +--- + +# Code Formatting Guidelines + +This document outlines the code formatting standards for Swift and Objective-C code in the SideStore project. Consistent formatting makes the codebase more readable and maintainable. + +## General Principles + +- **Consistency**: Follow the existing code style in the project +- **Readability**: Code should be easy to read and understand +- **Simplicity**: Prefer simple, clear solutions over clever ones +- **Apple conventions**: Follow Apple's official Swift and Objective-C conventions where possible + +## Swift Formatting + +### Naming Conventions + +#### Variables and Functions +- Use **camelCase** for variables, functions, and methods +- Use descriptive names that clearly indicate purpose + +```swift +// ✅ Good +let downloadProgress: Float +func validateUserCredentials() -> Bool +var isAppInstalling: Bool + +// ❌ Bad +let dp: Float +func validate() -> Bool +var installing: Bool +``` + +#### Classes, Structs, Enums, and Protocols +- Use **PascalCase** for types +- Use descriptive, noun-based names + +```swift +// ✅ Good +class AppInstaller +struct InstallationProgress +enum AppState +protocol AppManaging + +// ❌ Bad +class installer +struct progress +enum state +protocol managing +``` + +#### Constants +- Use **camelCase** for constants +- Consider using **SCREAMING_SNAKE_CASE** for global constants + +```swift +// ✅ Good +let maxRetryAttempts = 3 +private let defaultTimeout: TimeInterval = 30.0 + +// Global constants +let MAX_CONCURRENT_DOWNLOADS = 5 + +// ❌ Bad +let MaxRetryAttempts = 3 +let max_retry_attempts = 3 +``` + +### Indentation and Spacing + +#### Indentation +- Use **4 spaces** for indentation (not tabs) +- Align continuation lines with the opening delimiter + +```swift +// ✅ Good +func installApp(withIdentifier identifier: String, + sourceURL: URL, + completion: @escaping (Result) -> Void) { + // Implementation +} + +// ❌ Bad +func installApp(withIdentifier identifier: String, +sourceURL: URL, +completion: @escaping (Result) -> Void) { +// Implementation +} +``` + +#### Line Length +- Keep lines under **120 characters** when possible +- Break long lines at logical points + +```swift +// ✅ Good +let longVariableName = SomeClass.createInstanceWithVeryLongMethodName( + parameter1: value1, + parameter2: value2 +) + +// ❌ Bad +let longVariableName = SomeClass.createInstanceWithVeryLongMethodName(parameter1: value1, parameter2: value2, parameter3: value3) +``` + +#### Spacing +- Use single spaces around operators +- No trailing whitespace +- Single empty line between functions and major code sections + +```swift +// ✅ Good +let result = value1 + value2 +if condition && anotherCondition { + // Code +} + +func firstFunction() { + // Implementation +} + +func secondFunction() { + // Implementation +} + +// ❌ Bad +let result=value1+value2 +if condition&&anotherCondition{ + // Code +} +func firstFunction(){ + // Implementation +} +func secondFunction(){ + // Implementation +} +``` + +### Braces and Control Flow + +#### Brace Style +- Opening brace on the same line as the statement +- Closing brace on its own line, aligned with the opening statement + +```swift +// ✅ Good +if condition { + doSomething() +} else { + doSomethingElse() +} + +class MyClass { + func myMethod() { + // Implementation + } +} + +// ❌ Bad +if condition +{ + doSomething() +} +else +{ + doSomethingElse() +} +``` + +#### Guard Statements +- Use guard statements for early returns +- Keep guard conditions simple and readable + +```swift +// ✅ Good +guard let url = URL(string: urlString) else { + completion(.failure(ValidationError.invalidURL)) + return +} + +guard !apps.isEmpty else { + return +} + +// ❌ Bad +if let url = URL(string: urlString) { + // Long nested code block +} else { + completion(.failure(ValidationError.invalidURL)) + return +} +``` + +### Type Annotations and Inference + +#### When to Use Type Annotations +- Use type annotations when the type isn't obvious +- Omit type annotations when Swift can clearly infer the type + +```swift +// ✅ Good +let name = "SideStore" // Type is obvious +let timeout: TimeInterval = 30 // Type clarifies intent +var apps: [App] = [] // Empty array needs type annotation + +// ❌ Bad +let name: String = "SideStore" // Redundant type annotation +let timeout = 30 // Unclear if Int or TimeInterval +``` + +### Function Declarations + +#### Parameter Labels +- Use descriptive parameter labels +- Omit first parameter label when it reads naturally + +```swift +// ✅ Good +func install(_ app: App, to device: Device) +func download(from url: URL, completion: @escaping (Data?) -> Void) + +// ❌ Bad +func install(app: App, device: Device) +func download(url: URL, completion: @escaping (Data?) -> Void) +``` + +#### Return Types +- Put return type on the same line when possible +- Break to new line for very long signatures + +```swift +// ✅ Good +func processData() -> Result + +func complexFunction(withManyParameters param1: String, + param2: Int, + param3: Bool) + -> Result { + // Implementation +} +``` + +## Objective-C Formatting + +### Naming Conventions + +#### Methods +- Use descriptive method names with clear parameter labels +- Start with lowercase letter +- Use camelCase + +```objc +// ✅ Good +- (void)installAppWithIdentifier:(NSString *)identifier + sourceURL:(NSURL *)sourceURL + completion:(void (^)(NSError *error))completion; + +// ❌ Bad +- (void)install:(NSString *)id url:(NSURL *)url completion:(void (^)(NSError *))completion; +``` + +#### Variables and Properties +- Use camelCase +- Use descriptive names +- Prefix instance variables with underscore + +```objc +// ✅ Good +@interface AppManager : NSObject +@property (nonatomic, strong) NSArray *installedApps; +@property (nonatomic, assign) BOOL isInstalling; +@end + +@implementation AppManager { + NSURLSession *_networkSession; + dispatch_queue_t _processingQueue; +} +``` + +#### Classes and Protocols +- Use PascalCase +- Consider using prefixes for public classes (e.g., SS for SideStore) + +```objc +// ✅ Good +@interface SSAppInstaller : NSObject +@protocol SSAppManaging + +// ❌ Bad +@interface appInstaller : NSObject +@protocol appManaging +``` + +### Spacing and Formatting + +#### Method Declarations +- Align parameters vertically +- Use consistent spacing + +```objc +// ✅ Good +- (instancetype)initWithIdentifier:(NSString *)identifier + title:(NSString *)title + version:(NSString *)version; + +// ❌ Bad +- (instancetype)initWithIdentifier:(NSString *)identifier title:(NSString *)title version:(NSString *)version; +``` + +#### Braces +- Opening brace on the same line +- Closing brace on its own line + +```objc +// ✅ Good +if (condition) { + [self doSomething]; +} else { + [self doSomethingElse]; +} + +// ❌ Bad +if (condition) +{ + [self doSomething]; +} +else +{ + [self doSomethingElse]; +} +``` + +## Comments and Documentation + +### Swift Documentation +- Use `///` for documentation comments +- Include parameter and return value descriptions for public APIs + +```swift +/// Downloads and installs an app from the specified URL +/// - Parameters: +/// - identifier: The unique identifier for the app +/// - sourceURL: The URL to download the app from +/// - completion: Called when installation completes or fails +/// - Returns: A cancellable operation +func installApp(withIdentifier identifier: String, + sourceURL: URL, + completion: @escaping (Result) -> Void) -> Operation { + // Implementation +} +``` + +### Objective-C Documentation +- Use `/** */` for documentation comments +- Follow HeaderDoc or Doxygen conventions + +```objc +/** + * Downloads and installs an app from the specified URL + * @param identifier The unique identifier for the app + * @param sourceURL The URL to download the app from + * @param completion Block called when installation completes or fails + */ +- (void)installAppWithIdentifier:(NSString *)identifier + sourceURL:(NSURL *)sourceURL + completion:(void (^)(NSError *error))completion; +``` + +### Inline Comments +- Use `//` for single-line comments +- Keep comments concise and relevant +- Explain **why**, not **what** + +```swift +// ✅ Good +// Retry failed downloads up to 3 times to handle temporary network issues +let maxRetryAttempts = 3 + +// ❌ Bad +// Set maxRetryAttempts to 3 +let maxRetryAttempts = 3 +``` + +## Error Handling + +### Swift Error Handling +- Use Swift's native error handling with `throws` and `Result` types +- Create meaningful error types + +```swift +enum InstallationError: Error { + case invalidURL + case networkFailure(Error) + case insufficientStorage + case deviceNotSupported +} + +func installApp() throws -> App { + guard let url = URL(string: urlString) else { + throw InstallationError.invalidURL + } + // Implementation +} +``` + +### Objective-C Error Handling +- Use `NSError **` parameter pattern +- Always check if error parameter is non-nil before setting + +```objc +- (BOOL)installAppWithError:(NSError **)error { + if (someCondition) { + if (error) { + *error = [NSError errorWithDomain:SSErrorDomain + code:SSErrorCodeInvalidInput + userInfo:nil]; + } + return NO; + } + return YES; +} +``` + +## Best Practices + +### Memory Management +- Use ARC properly in both Swift and Objective-C +- Be careful with retain cycles; use `weak` and `unowned` references appropriately + +```swift +// ✅ Good +class AppInstaller { + weak var delegate: AppInstallerDelegate? + + private lazy var networkManager: NetworkManager = { + let manager = NetworkManager() + manager.delegate = self // Self is strong reference, but manager doesn't retain self + return manager + }() +} +``` + +### Threading +- Always update UI on the main queue +- Use appropriate queues for background work + +```swift +// ✅ Good +DispatchQueue.global(qos: .userInitiated).async { + let result = self.processData() + DispatchQueue.main.async { + self.updateUI(with: result) + } +} +``` + +### Optional Handling +- Use safe unwrapping techniques +- Prefer guard statements for early returns + +```swift +// ✅ Good +guard let data = response.data, + let apps = try? JSONDecoder().decode([App].self, from: data) else { + completion(.failure(ParsingError.invalidResponse)) + return +} +``` + +## Tools and Automation + +### SwiftLint +Consider using SwiftLint to automatically enforce many of these formatting rules: + +```yaml +# .swiftlint.yml +line_length: 120 +function_body_length: 60 +file_length: 400 +type_body_length: 300 + +disabled_rules: + - trailing_whitespace + +opt_in_rules: + - empty_count + - force_unwrapping +``` + +Remember: These guidelines should help code readability and maintainability. When in doubt, you should prioritize clarity and consistency with the existing codebase. \ No newline at end of file diff --git a/sidebars.js b/sidebars.js index 87772bde..a64eaf5d 100644 --- a/sidebars.js +++ b/sidebars.js @@ -36,6 +36,11 @@ const sidebars = { label: 'Advanced', items: ['advanced/anisette', 'advanced/app-sources', 'advanced/url-schema', 'advanced/sparserestore', 'advanced/jit'], }, + { + type: 'category', + label: 'Contributing', + items: ['contributing/contributing', 'contributing/formatting', 'contributing/advanced-formatting'], + }, 'release-notes', ], };