diff --git a/CHANGELOG.md b/CHANGELOG.md index 48161a4..bbdd1d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,11 +10,13 @@ The format of this changelog is based on [Keep a Changelog](https://keepachangel - `settings.gradle` and `settings.gradle.kts` are tagged `gradle` and `build`. They declare project structure rather than dependencies, so they do not carry `dependencies`. - `.mm` now reports as `Objective-C++`, a new language. It was missing from the extension table and reported `Unknown`. - Swift Package Manager files are recognised. `Package.swift` is tagged `build`, `dependencies`, `Swift` and `SwiftPM`; its lock file `Package.resolved` is tagged `dependencies`, `Swift` and `SwiftPM` and reports as `JSON` instead of `Unknown`. A Swift package previously looked like it declared no dependencies. `Swift` and `SwiftPM` are new tags. + - CocoaPods files are recognised. `Podfile` and `Podfile.lock` are tagged `dependencies` and `CocoaPods`, and `Podfile` reports as `Ruby` instead of `Unknown`. + - Carthage files are recognised. `Cartfile`, `Cartfile.private` and `Cartfile.resolved` are tagged `dependencies` and `Carthage`, and report as a new `Cartfile` filetype instead of `Unknown`. `CocoaPods` and `Carthage` are new tags. Neither tool's files carry a language tag, since both serve Swift and Objective-C alike. All of these are new tags on previously untagged files, so kospex is compatible either way; a re-sync is needed to pick them up. ### Changed - - The tag vocabulary grew from 119 to 121 tags, the filetype vocabulary from 86 to 87 and the language vocabulary from 32 to 33. + - The tag vocabulary grew from 119 to 123 tags, the filetype vocabulary from 86 to 88 and the language vocabulary from 32 to 33. - The Click floor moved from `>=8.3.1` to `>=8.3.3`, so an install can no longer resolve to a vulnerable Click release. - The package author email is now `peter@kospex.io`. diff --git a/changes/2026-10-cocoapods-and-carthage.md b/changes/2026-10-cocoapods-and-carthage.md new file mode 100644 index 0000000..2c5bcbf --- /dev/null +++ b/changes/2026-10-cocoapods-and-carthage.md @@ -0,0 +1,125 @@ +# CocoaPods and Carthage Detection + +Closes [#42](https://github.com/kospex/panopticas/issues/42). + +## Summary + +Neither dependency manager's files were recognised. Only `Podfile.lock` had a +filetype, and only through the generic `.lock` extension: + + Podfile [] 'Unknown' + Podfile.lock [] 'Lock' + Cartfile [] 'Unknown' + Cartfile.private [] 'Unknown' + Cartfile.resolved [] 'Unknown' + +An iOS or macOS project using either tool therefore looked like it declared no +dependencies. This completes the Apple-platform dependency managers begun with +Swift Package Manager in #38. + +## Changes + +Five entries in `METADATA_RULES["exact_filename_rules"]`: + + "podfile": ["dependencies", "CocoaPods"] + "podfile.lock": ["dependencies", "CocoaPods"] + "cartfile": ["dependencies", "Carthage"] + "cartfile.private": ["dependencies", "Carthage"] + "cartfile.resolved": ["dependencies", "Carthage"] + +Two extensionless entries in `EXT_FILETYPES`: + + "podfile": "Ruby" + "cartfile": "Cartfile" + +Two entries in `LANGUAGE_BY_BASENAME`, and `Cartfile` added to +`NON_LANGUAGE_FILETYPES`: + + "cartfile.private": "Cartfile" + "cartfile.resolved": "Cartfile" + +Result: + + Podfile ['dependencies', 'CocoaPods'] 'Ruby' + Podfile.lock ['dependencies', 'CocoaPods'] 'Lock' + Cartfile ['dependencies', 'Carthage'] 'Cartfile' + Cartfile.private ['dependencies', 'Carthage'] 'Cartfile' + Cartfile.resolved ['dependencies', 'Carthage'] 'Cartfile' + +## Sources + +| Convention | Source | +|---|---| +| The file is named `Podfile` and describes target dependencies | [The Podfile](https://guides.cocoapods.org/using/the-podfile.html) — "The file should simply be named `Podfile`." | +| `Podfile.lock` records the installed version of each pod and is kept in version control | [Using CocoaPods](https://guides.cocoapods.org/using/using-cocoapods.html) | +| An extensionless `Podfile` is loaded as Ruby | [`podfile.rb` in CocoaPods/Core](https://github.com/CocoaPods/Core/blob/master/lib/cocoapods-core/podfile.rb) — `from_file` sends `''`, `.podfile` and `.rb` to `from_ruby` | +| `Cartfile`, `Cartfile.private`, `Cartfile.resolved` and the OGDL-subset syntax | [Carthage Artifacts](https://github.com/Carthage/Carthage/blob/master/Documentation/Artifacts.md) | + +## Background: what Carthage is + +Recorded because it explains the tagging choices and took some checking. +Verified on 2026-10-09 against the sources named in each row. + +| Fact | Detail | Source | +|---|---|---| +| Apple platforms only | Describes itself as "a simple, decentralized dependency manager for Cocoa" | [Repository description](https://github.com/Carthage/Carthage) | +| Platforms | macOS, iOS, tvOS and watchOS | [README](https://github.com/Carthage/Carthage/blob/master/README.md) | +| visionOS | Supported in code — the SDK list includes visionOS and its simulator — but not mentioned in the README | [`Source/XCDBLD/SDK.swift`](https://github.com/Carthage/Carthage/blob/master/Source/XCDBLD/SDK.swift) | +| Languages | "frameworks written in Swift or Objective-C" | README | +| No central registry | "There is no central list of projects" | README | +| Linux and server-side Swift | Not stated either way. Unsupported by inference only: Carthage builds with Xcode's tools | — | +| Maintenance | Not archived; latest release is 0.40.0, September 2024 | [Releases](https://github.com/Carthage/Carthage/releases) | + +**Apple does not document Carthage.** It is a community project. A search of +developer.apple.com finds only forum threads and passing mentions in two WWDC +sessions; the +[WWDC22 Xcode Cloud session](https://developer.apple.com/videos/play/wwdc2022/110375/) +refers to CocoaPods and Carthage as third-party dependency managers needing +custom build scripts. Swift Package Manager is the only dependency manager +Apple documents as its own. There is therefore no Apple source to verify a +Carthage convention against — Carthage's own repository is the authority. + +Carthage, CocoaPods and Swift Package Manager all serve the same ecosystem. +Swift Package Manager is the default for new projects, so `Cartfile` and +`Podfile` are most often found in older codebases. + +## Design notes + +- **No language tag.** Both tools manage dependencies for Swift and + Objective-C projects alike, so a `Swift` tag would be wrong as often as + right. `Package.swift` carries `Swift` because the manifest itself is Swift. +- **No `build` tag.** These files declare and pin dependencies; they do not + describe a build. This matches `package.json` and `uv.lock`. +- **`Podfile` is `Ruby`.** The CocoaPods guides do not name the language, but + the loader evaluates an extensionless `Podfile` as Ruby. This follows the + existing `Steepfile` and `.rufo` mappings. A Podfile now counts towards a + repository's Ruby total. +- **`Cartfile` gets its own filetype.** Carthage documents the syntax as a + restricted subset of OGDL, which panopticas has no name for, so the file + takes its own name as the other tool-specific formats do. The Carthage + documentation does not name a syntax for `Cartfile.resolved`; it is given + the same filetype as the file it is generated from. +- **`Cartfile.private` and `Cartfile.resolved` are mapped by basename.** + `.private` and `.resolved` are not mapped as extensions — `Package.resolved` + is JSON, and `.private` means nothing in general. Tests guard that other + files with those extensions are not claimed. +- **`Podfile.lock` keeps the `Lock` filetype** it already had from its + extension, like `uv.lock`. +- `CocoaPods` and `Carthage` are new tags and `Cartfile` a new filetype. All + additive, so kospex is unaffected until a re-sync. + +## Not included + +- `*.podspec` and `*.podspec.json`, which describe a library published as a + pod rather than a project's dependencies. +- CocoaPods' alternative Podfile names (`CocoaPods.podfile`, + `CocoaPods.podfile.yaml`, `Podfile.rb`). The loader accepts them, but the + guides document only `Podfile`. +- The `Pods/` and `Carthage/` directories. + +## Tests + +`tests/test_panopticas.py` covers the filetype and tags for all five files, a +subdirectory path, and guards against look-alike names. Fixtures in +`src/tests/`: `Podfile`, `Podfile.lock`, `Cartfile`, `Cartfile.private` and +`Cartfile.resolved`. diff --git a/changes/2026-10-objective-cpp-and-swiftpm.md b/changes/2026-10-objective-cpp-and-swiftpm.md index 20e80fb..edc8a36 100644 --- a/changes/2026-10-objective-cpp-and-swiftpm.md +++ b/changes/2026-10-objective-cpp-and-swiftpm.md @@ -47,6 +47,12 @@ Result: project `Package.resolved` lives inside the project bundle, under `.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/`, and is tagged there too. +- **`SwiftPM` is the tool's own short name**, not a panopticas convention. + The official documentation is titled + [Package Manager (SwiftPM)](https://docs.swift.org/latest/documentation/packagemanagerdocs/) + — set as the display name in + [`Documentation.md`](https://github.com/swiftlang/swift-package-manager/blob/main/Sources/PackageManagerDocs/Documentation.docc/Documentation.md) + — and the project README refers to "SwiftPM's bug tracker". - **`Swift` and `SwiftPM` are new tags** and `Objective-C++` a new language name. `Swift` existed as a language but had never been used as a tag. All are additions, so kospex is unaffected until a re-sync. diff --git a/docs/api.md b/docs/api.md index c0fa107..ff16f66 100644 --- a/docs/api.md +++ b/docs/api.md @@ -53,7 +53,7 @@ EXT_FILETYPES["dockerfile"] # "Dockerfile" (special case) `EXT_FILETYPES` mixes two kinds of value: things that are programming languages (`Python`, `Go`) and things that are file types but not languages (`PNG`, `Gitignore`, `Lock`). These two collections classify every value in the table — -33 languages and 54 non-languages. `get_languages()` and `get_filetypes()` read +33 languages and 55 non-languages. `get_languages()` and `get_filetypes()` read from them. ```python @@ -369,7 +369,7 @@ Return every tag panopticas can assign to a file. from panopticas.core import get_tags get_tags() -# Returns: ['.NET', 'agent', 'Agents', 'AI', 'Aider', ..., 'yarn'] (121 tags) +# Returns: ['.NET', 'agent', 'Agents', 'AI', 'Aider', ..., 'yarn'] (123 tags) ``` **Returns:** Sorted list of tag strings, case-insensitively ordered @@ -386,7 +386,7 @@ Return every file type panopticas recognises, languages or not. from panopticas.core import get_filetypes get_filetypes() -# Returns: ['Apache JMeter', 'ASP.NET', ..., 'ZIP'] (87 file types) +# Returns: ['Apache JMeter', 'ASP.NET', ..., 'ZIP'] (88 file types) ``` **Returns:** Sorted list of file type strings diff --git a/docs/cli.md b/docs/cli.md index b55c86b..1dd545c 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -382,7 +382,7 @@ Amazon Q Apache ASP.NET Augment Azure DevOps binary Biome Bitbucket build Buildkite ... -121 tags +123 tags ``` Values are sorted case-insensitively, so `binary` sorts near `Bitbucket` @@ -395,9 +395,9 @@ count. | Command | Shape | |---|---| -| `tags` | `{"tags": [...], "count": 121}` | +| `tags` | `{"tags": [...], "count": 123}` | | `languages` | `{"languages": [...], "count": 33}` | -| `filetypes` | `{"filetypes": [...], "count": 87}` | +| `filetypes` | `{"filetypes": [...], "count": 88}` | ```console $ panopticas languages --json | jq -r '.count' diff --git a/src/panopticas/constants.py b/src/panopticas/constants.py index 11c9ae8..4515e9a 100644 --- a/src/panopticas/constants.py +++ b/src/panopticas/constants.py @@ -131,6 +131,8 @@ "makefile": "Makefile", "pylintrc": "INI", "steepfile": "Ruby", + "podfile": "Ruby", # CocoaPods loads an extensionless Podfile as Ruby + "cartfile": "Cartfile", # Carthage's own syntax, a restricted subset of OGDL "cname": "CNAME", # Often GitHub et al will use a CNAME file for a URL to host from } @@ -148,6 +150,10 @@ # Swift Package Manager's lock file is JSON. Mapped by basename because # ".resolved" is shared with Carthage's Cartfile.resolved, which is not. "package.resolved": "JSON", + # Carthage's other two files share the Cartfile syntax. Mapped by basename + # because ".private" and ".resolved" mean nothing on their own. + "cartfile.private": "Cartfile", + "cartfile.resolved": "Cartfile", } # Classification of every value in EXT_FILETYPES and LANGUAGE_BY_BASENAME as @@ -233,6 +239,7 @@ "ReStructuredText", "Text", # Named single-purpose files — a filename convention, not a format + "Cartfile", "CNAME", "CODEOWNERS", "Dockerignore", @@ -313,6 +320,13 @@ "pnpm-lock.yaml": ["dependencies", "JavaScript", "pnpm", "npm"], "package.swift": ["build", "dependencies", "Swift", "SwiftPM"], "package.resolved": ["dependencies", "Swift", "SwiftPM"], + # CocoaPods and Carthage serve Swift and Objective-C alike, so + # neither carries a language tag. + "podfile": ["dependencies", "CocoaPods"], + "podfile.lock": ["dependencies", "CocoaPods"], + "cartfile": ["dependencies", "Carthage"], + "cartfile.private": ["dependencies", "Carthage"], + "cartfile.resolved": ["dependencies", "Carthage"], ".gitattributes": ["Git"], ".gitlab-ci.yml": ["pipeline", "GitLab"], # Three letter YAML extension ".gitlab-ci.yaml": ["pipeline", "GitLab"], # Full four letter YAML extension diff --git a/src/tests/Cartfile b/src/tests/Cartfile new file mode 100644 index 0000000..7e497ef --- /dev/null +++ b/src/tests/Cartfile @@ -0,0 +1 @@ +github "Alamofire/Alamofire" ~> 5.8 diff --git a/src/tests/Cartfile.private b/src/tests/Cartfile.private new file mode 100644 index 0000000..cda2697 --- /dev/null +++ b/src/tests/Cartfile.private @@ -0,0 +1 @@ +github "Quick/Quick" ~> 7.0 diff --git a/src/tests/Cartfile.resolved b/src/tests/Cartfile.resolved new file mode 100644 index 0000000..4d78432 --- /dev/null +++ b/src/tests/Cartfile.resolved @@ -0,0 +1,2 @@ +github "Alamofire/Alamofire" "5.8.1" +github "Quick/Quick" "v7.3.0" diff --git a/src/tests/Podfile b/src/tests/Podfile new file mode 100644 index 0000000..9120d73 --- /dev/null +++ b/src/tests/Podfile @@ -0,0 +1,6 @@ +platform :ios, '15.0' + +target 'Example' do + use_frameworks! + pod 'Alamofire', '~> 5.8' +end diff --git a/src/tests/Podfile.lock b/src/tests/Podfile.lock new file mode 100644 index 0000000..54a3485 --- /dev/null +++ b/src/tests/Podfile.lock @@ -0,0 +1,16 @@ +PODS: + - Alamofire (5.8.1) + +DEPENDENCIES: + - Alamofire (~> 5.8) + +SPEC REPOS: + trunk: + - Alamofire + +SPEC CHECKSUMS: + Alamofire: 0000000000000000000000000000000000000000 + +PODFILE CHECKSUM: 0000000000000000000000000000000000000000 + +COCOAPODS: 1.15.2 diff --git a/tests/test_panopticas.py b/tests/test_panopticas.py index 18479b3..b02cff6 100644 --- a/tests/test_panopticas.py +++ b/tests/test_panopticas.py @@ -83,6 +83,14 @@ def test_kotlin(self): def test_kotlin_script(self): assert get_extension_filetype(".kts") == "Kotlin" + def test_podfile_is_ruby(self): + # CocoaPods loads an extensionless Podfile as Ruby. + assert get_extension_filetype("Podfile") == "Ruby" + + def test_cartfile_has_its_own_filetype(self): + # Carthage documents its own syntax, a restricted subset of OGDL. + assert get_extension_filetype("Cartfile") == "Cartfile" + def test_objective_cpp(self): assert get_extension_filetype(".mm") == "Objective-C++" @@ -221,8 +229,15 @@ def test_swiftpm_lock_file_is_json(self): # Carthage's Cartfile.resolved shares the extension and is plain text. assert get_language_edge_cases("Package.resolved") == "JSON" - def test_other_resolved_files_are_not_claimed(self): - assert get_language_edge_cases("Cartfile.resolved") is None + def test_carthage_lock_file_is_not_json(self): + assert get_language_edge_cases("Cartfile.resolved") == "Cartfile" + + def test_carthage_private_manifest(self): + assert get_language_edge_cases("Cartfile.private") == "Cartfile" + + def test_other_resolved_and_private_files_are_not_claimed(self): + assert get_language_edge_cases("Other.resolved") is None + assert get_language_edge_cases("key.private") is None def test_go_mod_in_path(self): assert get_language_edge_cases("/some/path/go.mod") == "go.mod" @@ -415,6 +430,25 @@ def test_gradle_kotlin_dsl_in_subdirectory(self): assert "gradle" in tags assert "dependencies" in tags + @pytest.mark.parametrize("filename", ["Podfile", "Podfile.lock"]) + def test_cocoapods_files(self, filename): + assert set(get_filename_metatypes(filename)) == \ + {"dependencies", "CocoaPods"} + + @pytest.mark.parametrize("filename", [ + "Cartfile", "Cartfile.private", "Cartfile.resolved", + ]) + def test_carthage_files(self, filename): + assert set(get_filename_metatypes(filename)) == \ + {"dependencies", "Carthage"} + + def test_cocoapods_file_in_subdirectory(self): + assert "CocoaPods" in get_filename_metatypes("ios/Podfile") + + def test_podfile_lookalikes_are_not_claimed(self): + assert get_filename_metatypes("Podfile.bak") == [] + assert get_filename_metatypes("MyCartfile") == [] + def test_swiftpm_manifest(self): assert set(get_filename_metatypes("Package.swift")) == \ {"build", "dependencies", "Swift", "SwiftPM"} @@ -742,6 +776,18 @@ def test_gradle_kotlin_settings_detected(self, fixtures_exist): def test_objective_cpp_detected(self, fixtures_exist): assert get_language("file.mm") == "Objective-C++" + def test_podfile_detected(self, fixtures_exist): + assert get_language("Podfile") == "Ruby" + + def test_podfile_lock_detected(self, fixtures_exist): + assert get_language("Podfile.lock") == "Lock" + + @pytest.mark.parametrize("filename", [ + "Cartfile", "Cartfile.private", "Cartfile.resolved", + ]) + def test_carthage_files_detected(self, fixtures_exist, filename): + assert get_language(filename) == "Cartfile" + def test_swiftpm_manifest_detected(self, fixtures_exist): assert get_language("Package.swift") == "Swift"