Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

Expand Down
125 changes: 125 additions & 0 deletions changes/2026-10-cocoapods-and-carthage.md
Original file line number Diff line number Diff line change
@@ -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`.
6 changes: 6 additions & 0 deletions changes/2026-10-objective-cpp-and-swiftpm.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 3 additions & 3 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
6 changes: 3 additions & 3 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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'
Expand Down
14 changes: 14 additions & 0 deletions src/panopticas/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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
Expand Down Expand Up @@ -233,6 +239,7 @@
"ReStructuredText",
"Text",
# Named single-purpose files — a filename convention, not a format
"Cartfile",
"CNAME",
"CODEOWNERS",
"Dockerignore",
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/tests/Cartfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
github "Alamofire/Alamofire" ~> 5.8
1 change: 1 addition & 0 deletions src/tests/Cartfile.private
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
github "Quick/Quick" ~> 7.0
2 changes: 2 additions & 0 deletions src/tests/Cartfile.resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github "Alamofire/Alamofire" "5.8.1"
github "Quick/Quick" "v7.3.0"
6 changes: 6 additions & 0 deletions src/tests/Podfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
platform :ios, '15.0'

target 'Example' do
use_frameworks!
pod 'Alamofire', '~> 5.8'
end
16 changes: 16 additions & 0 deletions src/tests/Podfile.lock
Original file line number Diff line number Diff line change
@@ -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
50 changes: 48 additions & 2 deletions tests/test_panopticas.py
Original file line number Diff line number Diff line change
Expand Up @@ -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++"

Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"}
Expand Down Expand Up @@ -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"

Expand Down
Loading