Skip to content

Commit 2336e25

Browse files
suraj-jadhav-otsuraj-jadhav
andauthored
7752 : Add UV CLI support for generating lock file from manifest on client side (#314)
* 7752 : Add UV CLI support for generating lock file from manifest on client side * 7752 : Add UV CLI support for generating lock file from manifest on client side * 7752 : Add UV CLI support for generating lock file from manifest on client side * 7752 : Add UV CLI support for generating lock file from manifest on client side * 7752 : Add UV CLI support for generating lock file from manifest on client side * 7752 : Lint issues * 7752 : Batch - Cyclop issue * Error: Lint issues * Error: Docker images * 7752: Fix tests * 7752: Fix tests * 7752: Revert Fix tests * 7752: Fix test * 7752: Fix test --------- Co-authored-by: suraj-jadhav <suraj.jadhav@debricked.com>
1 parent 2e3d7c3 commit 2336e25

File tree

16 files changed

+458
-22
lines changed

16 files changed

+458
-22
lines changed

.github/workflows/debricked.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,5 @@ jobs:
2222
- uses: GuillaumeFalourd/assert-command-line-output@v2.3
2323
with:
2424
command_line: go run cmd/debricked/main.go scan -t ${{ secrets.DEBRICKED_TOKEN }} -e "pkg/**" -e "test/**" -e "**/testdata/**"
25-
contains: AUTOMATION RULE
25+
contains: Successfully initialized scan
2626
expected_result: PASSED

build/docker/alpine.Dockerfile

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ RUN go mod download && go mod verify
66
COPY . .
77
RUN mkdir -p internal/file/embedded && \
88
wget -O internal/file/embedded/supported_formats.json https://debricked.com/api/1.0/open/files/supported-formats
9-
RUN apk add --no-cache make curl && make install && apk del make curl
9+
RUN apk add --no-cache make curl && \
10+
sed -i 's/\r$//' scripts/install.sh scripts/fetch_supported_formats.sh && \
11+
make install && \
12+
apk del make curl
1013
CMD [ "debricked" ]
1114

1215
FROM alpine:latest AS cli-base
@@ -88,8 +91,14 @@ RUN php -v && composer --version && sbt --version
8891

8992
# Install Poetry for Python resolution (pyproject.toml)
9093
RUN curl -sSL https://install.python-poetry.org | python3 - && \
91-
ln -s /root/.local/bin/poetry /usr/local/bin/poetry && \
92-
poetry --version
94+
ln -s /root/.local/bin/poetry /usr/local/bin/poetry && \
95+
poetry --version
96+
97+
# Install uv for Python resolution (pyproject.toml managed by uv)
98+
RUN curl -LsSf https://astral.sh/uv/install.sh | sh && \
99+
/root/.local/bin/uv --version
100+
101+
ENV PATH="/root/.local/bin:$PATH"
93102

94103
CMD [ "debricked", "scan" ]
95104

build/docker/debian.Dockerfile

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ RUN mkdir -p internal/file/embedded && \
1010
wget -O internal/file/embedded/supported_formats.json https://debricked.com/api/1.0/open/files/supported-formats
1111
RUN go mod download && go mod verify
1212
COPY . .
13-
RUN make install
13+
RUN sed -i 's/\r$//' scripts/install.sh scripts/fetch_supported_formats.sh && make install
1414
CMD [ "debricked" ]
1515

1616
FROM debian:bookworm-slim AS cli-base
@@ -99,6 +99,7 @@ RUN apt -y update && apt -y upgrade && apt -y install ca-certificates && \
9999
apt -y install -t unstable \
100100
python3.13 \
101101
python3.13-venv \
102+
python3-pip \
102103
openjdk-21-jdk && \
103104
apt -y clean && rm -rf /var/lib/apt/lists/* && \
104105
ln -s /usr/bin/python3.13 /usr/bin/python
@@ -139,6 +140,12 @@ RUN curl -sSL https://install.python-poetry.org | python3 - && \
139140
ln -s /root/.local/bin/poetry /usr/local/bin/poetry && \
140141
poetry --version
141142

143+
# Install uv for Python resolution (pyproject.toml managed by uv)
144+
RUN curl -LsSf https://astral.sh/uv/install.sh | sh && \
145+
/root/.local/bin/uv --version
146+
147+
ENV PATH="/root/.local/bin:$PATH"
148+
142149
CMD [ "debricked", "scan" ]
143150

144151
# Put copy at the end to speedup Docker build by caching previous RUNs and run those concurrently

internal/resolution/file/file_batch_factory.go

Lines changed: 89 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
package file
22

33
import (
4-
"path"
4+
"os"
5+
"path/filepath"
56
"regexp"
7+
"strings"
68

79
"github.com/debricked/cli/internal/resolution/pm"
810
"github.com/debricked/cli/internal/resolution/pm/npm"
11+
"github.com/debricked/cli/internal/resolution/pm/poetry"
12+
"github.com/debricked/cli/internal/resolution/pm/uv"
913
"github.com/debricked/cli/internal/resolution/pm/yarn"
1014
)
1115

@@ -29,35 +33,56 @@ func (bf *BatchFactory) SetNpmPreferred(npmPreferred bool) {
2933
bf.npmPreferred = npmPreferred
3034
}
3135

36+
//nolint:cyclop
3237
func (bf *BatchFactory) Make(files []string) []IBatch {
3338
batchMap := make(map[string]IBatch)
3439
for _, file := range files {
35-
for _, p := range bf.pms {
36-
if bf.skipPackageManager(p) {
37-
continue
38-
}
40+
bf.processFile(file, batchMap)
41+
}
3942

40-
for _, manifest := range p.Manifests() {
43+
batches := make([]IBatch, 0, len(batchMap))
44+
for _, batch := range batchMap {
45+
batches = append(batches, batch)
46+
}
47+
48+
return batches
49+
}
50+
51+
func (bf *BatchFactory) processFile(file string, batchMap map[string]IBatch) {
52+
base := filepath.Base(file)
53+
for _, p := range bf.pms {
54+
if bf.skipPackageManager(p) {
55+
continue
56+
}
57+
58+
for _, manifest := range p.Manifests() {
59+
if bf.shouldProcessManifest(manifest, base, file, p) {
4160
compiledRegex, _ := regexp.Compile(manifest)
42-
if compiledRegex.MatchString(path.Base(file)) {
43-
batch, ok := batchMap[p.Name()]
44-
if !ok {
45-
batch = NewBatch(p)
46-
batchMap[p.Name()] = batch
47-
}
48-
batch.Add(file)
61+
if compiledRegex.MatchString(base) {
62+
bf.addToBatch(p, file, batchMap)
4963
}
5064
}
5165
}
5266
}
67+
}
5368

54-
batches := make([]IBatch, 0, len(batchMap))
69+
func (bf *BatchFactory) shouldProcessManifest(manifest, base, file string, p pm.IPm) bool {
70+
if manifest == "pyproject.toml" && strings.EqualFold(base, "pyproject.toml") {
71+
pmName := detectPyprojectPm(file)
5572

56-
for _, batch := range batchMap {
57-
batches = append(batches, batch)
73+
return pmName == p.Name()
5874
}
5975

60-
return batches
76+
return true
77+
}
78+
79+
func (bf *BatchFactory) addToBatch(p pm.IPm, file string, batchMap map[string]IBatch) {
80+
batch, ok := batchMap[p.Name()]
81+
if !ok {
82+
batch = NewBatch(p)
83+
batchMap[p.Name()] = batch
84+
}
85+
batch.Add(file)
6186
}
6287

6388
func (bf *BatchFactory) skipPackageManager(p pm.IPm) bool {
@@ -72,3 +97,50 @@ func (bf *BatchFactory) skipPackageManager(p pm.IPm) bool {
7297

7398
return false
7499
}
100+
101+
func detectPyprojectPm(pyprojectPath string) string {
102+
dir := filepath.Dir(pyprojectPath)
103+
104+
if fileExists(filepath.Join(dir, "uv.lock")) {
105+
return uv.Name
106+
}
107+
108+
if fileExists(filepath.Join(dir, "poetry.lock")) {
109+
return poetry.Name
110+
}
111+
112+
content, err := os.ReadFile(pyprojectPath)
113+
if err == nil {
114+
data := string(content)
115+
hasPoetry := strings.Contains(data, "[tool.poetry]") ||
116+
strings.Contains(data, "tool.poetry")
117+
hasProject := strings.Contains(data, "[project]")
118+
119+
if hasPoetry && hasProject {
120+
// Ambiguous: both Poetry and UV indicators present
121+
122+
return ""
123+
}
124+
if hasPoetry {
125+
126+
return poetry.Name
127+
}
128+
if hasProject {
129+
130+
return uv.Name
131+
}
132+
}
133+
134+
// If no indicators found, cannot determine PM
135+
136+
return ""
137+
}
138+
139+
func fileExists(path string) bool {
140+
info, err := os.Stat(path)
141+
if err != nil {
142+
return false
143+
}
144+
145+
return !info.IsDir()
146+
}

internal/resolution/pm/pm.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/debricked/cli/internal/resolution/pm/pip"
1212
"github.com/debricked/cli/internal/resolution/pm/poetry"
1313
"github.com/debricked/cli/internal/resolution/pm/sbt"
14+
"github.com/debricked/cli/internal/resolution/pm/uv"
1415
"github.com/debricked/cli/internal/resolution/pm/yarn"
1516
)
1617

@@ -26,6 +27,7 @@ func Pms() []IPm {
2627
gomod.NewPm(),
2728
pip.NewPm(),
2829
poetry.NewPm(),
30+
uv.NewPm(),
2931
yarn.NewPm(),
3032
npm.NewPm(),
3133
bower.NewPm(),
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package uv
2+
3+
import (
4+
"os"
5+
"os/exec"
6+
"path/filepath"
7+
)
8+
9+
type ICmdFactory interface {
10+
MakeLockCmd(manifestFile string) (*exec.Cmd, error)
11+
}
12+
13+
type IExecPath interface {
14+
LookPath(file string) (string, error)
15+
}
16+
17+
type ExecPath struct{}
18+
19+
func (_ ExecPath) LookPath(file string) (string, error) {
20+
return exec.LookPath(file)
21+
}
22+
23+
type CmdFactory struct {
24+
execPath IExecPath
25+
}
26+
27+
func (cmdf CmdFactory) MakeLockCmd(manifestFile string) (*exec.Cmd, error) {
28+
uvPath, err := cmdf.execPath.LookPath("uv")
29+
if err != nil {
30+
return nil, err
31+
}
32+
33+
workingDir := filepath.Dir(filepath.Clean(manifestFile))
34+
35+
env := os.Environ()
36+
37+
return &exec.Cmd{
38+
Path: uvPath,
39+
Args: []string{"uv", "lock"},
40+
Dir: workingDir,
41+
Env: env,
42+
}, nil
43+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package uv
2+
3+
import (
4+
"path/filepath"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
type execPathMock struct{}
11+
12+
func (execPathMock) LookPath(file string) (string, error) {
13+
return "/usr/bin/" + file, nil
14+
}
15+
16+
func TestMakeLockCmd(t *testing.T) {
17+
factory := CmdFactory{execPath: execPathMock{}}
18+
manifest := filepath.Join("testdata", "pyproject.toml")
19+
20+
cmd, err := factory.MakeLockCmd(manifest)
21+
assert.NoError(t, err)
22+
assert.NotNil(t, cmd)
23+
assert.Equal(t, "/usr/bin/uv", cmd.Path)
24+
assert.Contains(t, cmd.Args, "uv")
25+
assert.Contains(t, cmd.Args, "lock")
26+
assert.Equal(t, filepath.Dir(manifest), cmd.Dir)
27+
}

internal/resolution/pm/uv/job.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package uv
2+
3+
import (
4+
"regexp"
5+
"strings"
6+
7+
"github.com/debricked/cli/internal/resolution/job"
8+
"github.com/debricked/cli/internal/resolution/pm/util"
9+
)
10+
11+
const (
12+
executableNotFoundErrRegex = `executable file not found`
13+
)
14+
15+
type Job struct {
16+
job.BaseJob
17+
cmdFactory ICmdFactory
18+
}
19+
20+
func NewJob(file string, cmdFactory ICmdFactory) *Job {
21+
return &Job{
22+
BaseJob: job.NewBaseJob(file),
23+
cmdFactory: cmdFactory,
24+
}
25+
}
26+
27+
func (j *Job) Run() {
28+
status := "generating uv.lock"
29+
j.SendStatus(status)
30+
31+
lockCmd, err := j.cmdFactory.MakeLockCmd(j.GetFile())
32+
if err != nil {
33+
j.handleError(j.createError(err.Error(), "", status))
34+
35+
return
36+
}
37+
38+
if output, err := lockCmd.Output(); err != nil {
39+
exitErr := j.GetExitError(err, string(output))
40+
errorMessage := strings.Join([]string{string(output), exitErr.Error()}, "")
41+
j.handleError(j.createError(errorMessage, lockCmd.String(), status))
42+
43+
return
44+
}
45+
}
46+
47+
func (j *Job) createError(errorStr string, cmd string, status string) job.IError {
48+
cmdError := util.NewPMJobError(errorStr)
49+
cmdError.SetCommand(cmd)
50+
cmdError.SetStatus(status)
51+
52+
return cmdError
53+
}
54+
55+
func (j *Job) handleError(cmdError job.IError) {
56+
expressions := []string{
57+
executableNotFoundErrRegex,
58+
}
59+
60+
for _, expression := range expressions {
61+
regex := regexp.MustCompile(expression)
62+
matches := regex.FindAllStringSubmatch(cmdError.Error(), -1)
63+
64+
if len(matches) > 0 {
65+
cmdError = j.addDocumentation(expression, matches, cmdError)
66+
j.Errors().Append(cmdError)
67+
68+
return
69+
}
70+
}
71+
72+
j.Errors().Append(cmdError)
73+
}
74+
75+
func (j *Job) addDocumentation(expr string, _ [][]string, cmdError job.IError) job.IError {
76+
documentation := cmdError.Documentation()
77+
78+
switch expr {
79+
case executableNotFoundErrRegex:
80+
documentation = j.GetExecutableNotFoundErrorDocumentation("uv")
81+
}
82+
83+
cmdError.SetDocumentation(documentation)
84+
85+
return cmdError
86+
}

0 commit comments

Comments
 (0)