Skip to content

Commit 5cc28fd

Browse files
bfirshclaude
andauthored
Fix wheel auto-detection and rework install to use symlinks (#2721)
* fix: resolve wheel auto-detection from executable path Also pick the latest wheel when multiple versions exist in dist/ (filepath.Glob returns lexicographic order, so use last match). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: replace make install with mise run install using symlinks Update development docs in CONTRIBUTING.md and README.md to document all three build components (SDK, coglet, CLI) and their build order. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: document coglet linux wheel build and llms.txt regeneration in AGENTS.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent efe9956 commit 5cc28fd

7 files changed

Lines changed: 117 additions & 57 deletions

File tree

AGENTS.md

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Development tasks are managed with [mise](https://mise.jdx.dev/). Run `mise task
3232
| `mise run build:cog` | Build cog CLI binary |
3333
| `mise run build:coglet` | Build coglet wheel (dev) |
3434
| `mise run build:sdk` | Build SDK wheel |
35+
| `mise run install` | Build and symlink cog to /usr/local/bin |
3536

3637
### Task Naming Convention
3738

@@ -70,6 +71,10 @@ Tasks follow a consistent naming pattern:
7071
- `mise run build:coglet:wheel:linux-arm64` - Build for Linux ARM64
7172
- `mise run build:sdk` - Build SDK wheel
7273

74+
**Install:**
75+
- `mise run install` - Symlink cog CLI to `/usr/local/bin` (requires `build:cog` first)
76+
- `PREFIX=/custom/path mise run install` - Symlink to custom location
77+
7378
**Other:**
7479
- `mise run typecheck` - Type check all languages
7580
- `mise run generate` - Run code generation
@@ -123,7 +128,7 @@ The CLI code is in the `cmd/cog/` and `pkg/` directories. Support tooling is in
123128
The main commands for working on the CLI are:
124129
- `go run ./cmd/cog` - Runs the Cog CLI directly from source (requires wheel to be built first)
125130
- `mise run build:cog` - Builds the Cog CLI binary, embedding the Python wheel
126-
- `make install` - Builds and installs the Cog CLI binary to `/usr/local/bin`, or to a custom path with `make install PREFIX=/custom/path`
131+
- `mise run install` - Symlinks the built binary to `/usr/local/bin` (run `build:cog` first), or to a custom path with `PREFIX=/custom/path mise run install`
127132
- `mise run test:go` - Runs all Go unit tests
128133
- `go test ./pkg/...` - Runs tests directly with `go test`
129134

@@ -144,7 +149,8 @@ The code is in the `crates/` directory:
144149
For detailed architecture documentation, see `crates/README.md` and `crates/coglet/README.md`.
145150

146151
The main commands for working on Coglet are:
147-
- `mise run build:coglet` - Build and install coglet wheel for development
152+
- `mise run build:coglet` - Build and install coglet wheel for development (macOS, for local Rust/Python tests)
153+
- `mise run build:coglet:wheel:linux-x64` - Build Linux x86_64 wheel (required to test Rust changes in Docker containers via `cog predict`/`cog train`)
148154
- `mise run test:rust` - Run Rust unit tests
149155
- `mise run lint:rust` - Run clippy linter
150156
- `mise run fmt:rust:fix` - Format code
@@ -164,16 +170,19 @@ The integration test suite in `integration-tests/` tests the end-to-end function
164170

165171
The integration tests require a built Cog binary, which defaults to the first `cog` in `PATH`. Run tests against a specific binary with the `COG_BINARY` environment variable:
166172
```bash
167-
make install PREFIX=./cog
168-
COG_BINARY=./cog/cog mise run test:integration
173+
mise run build:cog
174+
COG_BINARY=dist/go/*/cog mise run test:integration
169175
```
170176

171177
### Development Workflow
172178
1. Run `mise install` to set up the development environment
173179
2. Run `mise run build:sdk` after making changes to the `./python` directory
174-
3. Run `mise run fmt:fix` to format code
175-
4. Run `mise run lint` to check code quality
176-
5. Read the `./docs` directory and make sure the documentation is up to date
180+
3. Run `mise run build:coglet:wheel:linux-x64` after making changes to the `./crates` directory (needed for Docker testing)
181+
4. Run `mise run build:cog` to build the CLI (embeds the SDK wheel; picks up coglet wheel from `dist/`)
182+
5. Run `mise run fmt:fix` to format code
183+
6. Run `mise run lint` to check code quality
184+
7. Run `mise run docs:llm` to regenerate `docs/llms.txt` after changing `README.md` or any `docs/*.md` file
185+
8. Read the `./docs` directory and make sure the documentation is up to date
177186

178187
## Architecture
179188

CONTRIBUTING.md

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,33 @@ uv venv
2121
uv sync --all-groups
2222
```
2323

24-
### Common tasks
24+
### Building
25+
26+
Cog is composed of three components that are built separately:
27+
28+
- **Python SDK** (`python/cog/`) — the Python library that model authors use. Built into a wheel that gets installed inside containers.
29+
- **Coglet** (`crates/`) — a Rust prediction server that runs inside containers. Cross-compiled into a Linux wheel.
30+
- **Cog CLI** (`cmd/cog/`, `pkg/`) — the Go command-line tool. Embeds the SDK wheel and picks up the coglet wheel from `dist/`.
31+
32+
```sh
33+
# Build everything and install
34+
mise run build:sdk # build the Python SDK wheel
35+
mise run build:coglet:wheel:linux-x64 # cross-compile the coglet wheel for Linux containers
36+
mise run build:cog # build the Go CLI (embeds SDK, picks up coglet from dist/)
37+
sudo mise run install # symlink the binary to /usr/local/bin
38+
```
39+
40+
After making changes, rebuild only the component you changed and then `build:cog`:
2541

2642
```sh
27-
# Build cog binary
28-
mise run build:cog
43+
mise run build:sdk # after changing python/cog/
44+
mise run build:coglet:wheel:linux-x64 # after changing crates/
45+
mise run build:cog # after changing cmd/cog/ or pkg/, or to pick up new wheels
46+
```
2947

30-
# Install cog to $GOPATH/bin
31-
make install PREFIX=$(go env GOPATH)
48+
### Common tasks
3249

50+
```sh
3351
# Run all tests
3452
mise run test:go
3553
mise run test:python

Makefile

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,7 @@ $(info └───────────────────────
2424
$(info )
2525
endif
2626

27-
DESTDIR ?=
28-
PREFIX = /usr/local
29-
BINDIR = $(PREFIX)/bin
30-
31-
INSTALL := install -m 0755
27+
PREFIX ?= /usr/local
3228

3329
GO ?= go
3430

@@ -50,8 +46,7 @@ wheel:
5046

5147
.PHONY: install
5248
install: cog
53-
$(INSTALL) -d $(DESTDIR)$(BINDIR)
54-
$(INSTALL) cog $(DESTDIR)$(BINDIR)/cog
49+
PREFIX=$(PREFIX) mise run install
5550

5651
# =============================================================================
5752
# Test targets

README.md

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -156,13 +156,6 @@ sudo curl -o /usr/local/bin/cog -L "https://github.com/replicate/cog/releases/la
156156
sudo chmod +x /usr/local/bin/cog
157157
```
158158

159-
Alternatively, you can build Cog from source and install it with these commands:
160-
161-
```console
162-
make
163-
sudo make install
164-
```
165-
166159
Or if you are on docker:
167160

168161
```
@@ -179,6 +172,10 @@ brew upgrade cog
179172

180173
Otherwise, you can upgrade to the latest version by running the same commands you used to install it.
181174

175+
## Development
176+
177+
See [CONTRIBUTING.md](CONTRIBUTING.md) for how to set up a development environment and build from source.
178+
182179
## Next steps
183180

184181
- [Get started with an example model](docs/getting-started.md)

docs/llms.txt

Lines changed: 4 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mise.toml

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -91,15 +91,20 @@ run = [
9191
]
9292

9393
[tasks.install]
94-
description = "Build and install cog CLI to /usr/local/bin (or PREFIX)"
95-
depends = ["build:cog"]
94+
description = "Symlink cog CLI to /usr/local/bin (or PREFIX). Run build:cog first."
9695
run = """
9796
#!/usr/bin/env bash
9897
set -e
9998
PREFIX="${PREFIX:-/usr/local}"
100-
install -d "$PREFIX/bin"
101-
install -m 0755 cog "$PREFIX/bin/cog"
102-
echo "Installed cog to $PREFIX/bin/cog"
99+
BINARY=$(ls dist/go/*/cog 2>/dev/null | head -1)
100+
if [ -z "$BINARY" ]; then
101+
echo "Error: no cog binary found in dist/go/. Run 'mise run build:cog' first." >&2
102+
exit 1
103+
fi
104+
BINARY="$(cd "$(dirname "$BINARY")" && pwd)/$(basename "$BINARY")"
105+
mkdir -p "$PREFIX/bin"
106+
ln -sf "$BINARY" "$PREFIX/bin/cog"
107+
echo "Installed $PREFIX/bin/cog -> $BINARY"
103108
"""
104109

105110
[tasks."build:cog"]

pkg/wheels/wheels.go

Lines changed: 58 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -125,64 +125,103 @@ func getRepoRoot() (string, error) {
125125
return strings.TrimSpace(string(out)), nil
126126
}
127127

128+
// distFromExecutable returns the dist/ directory relative to the running cog
129+
// binary, if it appears to be in a goreleaser output layout (dist/go/<platform>/cog).
130+
// Returns empty string if the path cannot be determined.
131+
func distFromExecutable() string {
132+
exePath, err := os.Executable()
133+
if err != nil {
134+
return ""
135+
}
136+
exePath, err = filepath.EvalSymlinks(exePath)
137+
if err != nil {
138+
return ""
139+
}
140+
// Binary is at dist/go/<platform>/cog → go up 2 levels to dist/
141+
distDir := filepath.Clean(filepath.Join(filepath.Dir(exePath), "..", ".."))
142+
if info, err := os.Stat(distDir); err == nil && info.IsDir() {
143+
return distDir
144+
}
145+
return ""
146+
}
147+
128148
// findWheelInDist looks for a wheel file matching the pattern in the dist/ directory.
129149
// Returns the absolute path to the wheel if found.
130-
// Checks multiple locations: ./dist, <repo-root>/dist
150+
// Checks multiple locations: ./dist, <repo-root>/dist, <executable-relative>/dist
131151
// If platformTag is non-empty, only wheels whose filename contains the tag are considered.
152+
// When multiple wheels match, the last one in lexicographic order is used (highest version).
132153
func findWheelInDist(pattern string, envVar string, platformTag string) (string, error) {
133154
// First try ./dist in current directory
134155
matches, _ := filepath.Glob(filepath.Join("dist", pattern))
135156
matches = filterWheelsByPlatform(matches, platformTag)
136157
if len(matches) > 0 {
137-
absPath, err := filepath.Abs(matches[0])
158+
best := matches[len(matches)-1]
159+
absPath, err := filepath.Abs(best)
138160
if err != nil {
139-
return matches[0], nil
161+
return best, nil
140162
}
141163
return absPath, nil
142164
}
143165

144166
// Try repo root dist/
145167
repoRoot, err := getRepoRoot()
146-
if err != nil {
147-
return "", err
168+
if err == nil {
169+
distDir := filepath.Join(repoRoot, "dist")
170+
matches, _ = filepath.Glob(filepath.Join(distDir, pattern))
171+
matches = filterWheelsByPlatform(matches, platformTag)
172+
if len(matches) > 0 {
173+
return matches[len(matches)-1], nil
174+
}
148175
}
149176

150-
distDir := filepath.Join(repoRoot, "dist")
151-
matches, _ = filepath.Glob(filepath.Join(distDir, pattern))
152-
matches = filterWheelsByPlatform(matches, platformTag)
153-
if len(matches) > 0 {
154-
return matches[0], nil
177+
// Try dist/ relative to the cog executable
178+
if distDir := distFromExecutable(); distDir != "" {
179+
matches, _ = filepath.Glob(filepath.Join(distDir, pattern))
180+
matches = filterWheelsByPlatform(matches, platformTag)
181+
if len(matches) > 0 {
182+
return matches[len(matches)-1], nil
183+
}
155184
}
156185

157-
return "", fmt.Errorf("%s=dist: no wheel matching '%s' found in %s\n\nTo build the wheel, run: mise run build:sdk (for cog) or mise run build:coglet (for coglet)", envVar, pattern, distDir)
186+
return "", fmt.Errorf("%s=dist: no wheel matching '%s' found\n\nTo build the wheel, run: mise run build:sdk (for cog) or mise run build:coglet (for coglet)", envVar, pattern)
158187
}
159188

160189
// findWheelInDistSilent is like findWheelInDist but returns empty string instead of error.
161190
// Used for auto-detection where missing wheel is not an error.
162191
// If platformTag is non-empty, only wheels whose filename contains the tag are considered.
192+
// When multiple wheels match, the last one in lexicographic order is used (highest version).
163193
func findWheelInDistSilent(pattern string, platformTag string) string {
164194
// First try ./dist in current directory
165195
matches, _ := filepath.Glob(filepath.Join("dist", pattern))
166196
matches = filterWheelsByPlatform(matches, platformTag)
167197
if len(matches) > 0 {
168-
absPath, _ := filepath.Abs(matches[0])
198+
best := matches[len(matches)-1]
199+
absPath, _ := filepath.Abs(best)
169200
if absPath != "" {
170201
return absPath
171202
}
172-
return matches[0]
203+
return best
173204
}
174205

175206
// Try repo root dist/
176207
repoRoot, err := getRepoRoot()
177-
if err != nil {
178-
return ""
208+
if err == nil {
209+
matches, _ = filepath.Glob(filepath.Join(repoRoot, "dist", pattern))
210+
matches = filterWheelsByPlatform(matches, platformTag)
211+
if len(matches) > 0 {
212+
return matches[len(matches)-1]
213+
}
179214
}
180215

181-
matches, _ = filepath.Glob(filepath.Join(repoRoot, "dist", pattern))
182-
matches = filterWheelsByPlatform(matches, platformTag)
183-
if len(matches) > 0 {
184-
return matches[0]
216+
// Try dist/ relative to the cog executable
217+
if distDir := distFromExecutable(); distDir != "" {
218+
matches, _ = filepath.Glob(filepath.Join(distDir, pattern))
219+
matches = filterWheelsByPlatform(matches, platformTag)
220+
if len(matches) > 0 {
221+
return matches[len(matches)-1]
222+
}
185223
}
224+
186225
return ""
187226
}
188227

0 commit comments

Comments
 (0)