Skip to content
Open
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
12 changes: 9 additions & 3 deletions build/docker/alpine.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ RUN go mod download && go mod verify
COPY . .
RUN mkdir -p internal/file/embedded && \
wget -O internal/file/embedded/supported_formats.json https://debricked.com/api/1.0/open/files/supported-formats
RUN apk add --no-cache make curl && make install && apk del make curl
RUN apk add --no-cache make curl && sed -i 's/\r$//' scripts/install.sh scripts/fetch_supported_formats.sh && make install && apk del make curl
CMD [ "debricked" ]

FROM alpine:latest AS cli-base
Expand Down Expand Up @@ -88,8 +88,14 @@ RUN php -v && composer --version && sbt --version

# Install Poetry for Python resolution (pyproject.toml)
RUN curl -sSL https://install.python-poetry.org | python3 - && \
ln -s /root/.local/bin/poetry /usr/local/bin/poetry && \
poetry --version
ln -s /root/.local/bin/poetry /usr/local/bin/poetry && \
poetry --version

# Install uv for Python resolution (pyproject.toml managed by uv)
RUN curl -LsSf https://astral.sh/uv/install.sh | sh && \
/root/.cargo/bin/uv --version

ENV PATH="/root/.cargo/bin:$PATH"

CMD [ "debricked", "scan" ]

Expand Down
9 changes: 8 additions & 1 deletion build/docker/debian.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ RUN mkdir -p internal/file/embedded && \
wget -O internal/file/embedded/supported_formats.json https://debricked.com/api/1.0/open/files/supported-formats
RUN go mod download && go mod verify
COPY . .
RUN make install
RUN sed -i 's/\r$//' scripts/install.sh scripts/fetch_supported_formats.sh && make install
CMD [ "debricked" ]

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

# Install uv for Python resolution (pyproject.toml managed by uv)
RUN curl -LsSf https://astral.sh/uv/install.sh | sh && \
/root/.cargo/bin/uv --version

ENV PATH="/root/.cargo/bin:$PATH"

CMD [ "debricked", "scan" ]

# Put copy at the end to speedup Docker build by caching previous RUNs and run those concurrently
Expand Down
102 changes: 84 additions & 18 deletions internal/resolution/file/file_batch_factory.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package file

import (
"path"
"os"
"path/filepath"
"regexp"
"strings"

"github.com/debricked/cli/internal/resolution/pm"
"github.com/debricked/cli/internal/resolution/pm/npm"
"github.com/debricked/cli/internal/resolution/pm/poetry"
"github.com/debricked/cli/internal/resolution/pm/uv"
"github.com/debricked/cli/internal/resolution/pm/yarn"
)

Expand All @@ -29,35 +33,54 @@
bf.npmPreferred = npmPreferred
}

//nolint:cyclop
func (bf *BatchFactory) Make(files []string) []IBatch {
batchMap := make(map[string]IBatch)
for _, file := range files {
for _, p := range bf.pms {
if bf.skipPackageManager(p) {
continue
}
bf.processFile(file, batchMap)
}

batches := make([]IBatch, 0, len(batchMap))
for _, batch := range batchMap {
batches = append(batches, batch)
}

return batches
}

for _, manifest := range p.Manifests() {
func (bf *BatchFactory) processFile(file string, batchMap map[string]IBatch) {
base := filepath.Base(file)
for _, p := range bf.pms {
if bf.skipPackageManager(p) {
continue
}

for _, manifest := range p.Manifests() {
if bf.shouldProcessManifest(manifest, base, file, p) {
compiledRegex, _ := regexp.Compile(manifest)
if compiledRegex.MatchString(path.Base(file)) {
batch, ok := batchMap[p.Name()]
if !ok {
batch = NewBatch(p)
batchMap[p.Name()] = batch
}
batch.Add(file)
if compiledRegex.MatchString(base) {
bf.addToBatch(p, file, batchMap)
}
}
}
}
}

batches := make([]IBatch, 0, len(batchMap))

for _, batch := range batchMap {
batches = append(batches, batch)
func (bf *BatchFactory) shouldProcessManifest(manifest, base, file string, p pm.IPm) bool {
if manifest == "pyproject.toml" && strings.EqualFold(base, "pyproject.toml") {
pmName := detectPyprojectPm(file)
return pmName == p.Name()

Check failure on line 72 in internal/resolution/file/file_batch_factory.go

View workflow job for this annotation

GitHub Actions / Lint

return with no blank line before (nlreturn)
}
return true

Check failure on line 74 in internal/resolution/file/file_batch_factory.go

View workflow job for this annotation

GitHub Actions / Lint

return with no blank line before (nlreturn)
}

return batches
func (bf *BatchFactory) addToBatch(p pm.IPm, file string, batchMap map[string]IBatch) {
batch, ok := batchMap[p.Name()]
if !ok {
batch = NewBatch(p)
batchMap[p.Name()] = batch
}
batch.Add(file)
}

func (bf *BatchFactory) skipPackageManager(p pm.IPm) bool {
Expand All @@ -72,3 +95,46 @@

return false
}

func detectPyprojectPm(pyprojectPath string) string {
dir := filepath.Dir(pyprojectPath)

if fileExists(filepath.Join(dir, "uv.lock")) {
return uv.Name
}

if fileExists(filepath.Join(dir, "poetry.lock")) {
return poetry.Name
}

content, err := os.ReadFile(pyprojectPath)
if err == nil {
data := string(content)
hasPoetry := strings.Contains(data, "[tool.poetry]") ||
strings.Contains(data, "tool.poetry")
hasProject := strings.Contains(data, "[project]")

if hasPoetry && hasProject {
// Ambiguous: both Poetry and UV indicators present
return ""
}
if hasPoetry {
return poetry.Name
}
if hasProject {
return uv.Name
}
}

// If no indicators found, cannot determine PM
return ""
}

func fileExists(path string) bool {
info, err := os.Stat(path)
if err != nil {
return false
}

return !info.IsDir()
}
2 changes: 2 additions & 0 deletions internal/resolution/pm/pm.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/debricked/cli/internal/resolution/pm/pip"
"github.com/debricked/cli/internal/resolution/pm/poetry"
"github.com/debricked/cli/internal/resolution/pm/sbt"
"github.com/debricked/cli/internal/resolution/pm/uv"
"github.com/debricked/cli/internal/resolution/pm/yarn"
)

Expand All @@ -26,6 +27,7 @@ func Pms() []IPm {
gomod.NewPm(),
pip.NewPm(),
poetry.NewPm(),
uv.NewPm(),
yarn.NewPm(),
npm.NewPm(),
bower.NewPm(),
Expand Down
43 changes: 43 additions & 0 deletions internal/resolution/pm/uv/cmd_factory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package uv

import (
"os"
"os/exec"
"path/filepath"
)

type ICmdFactory interface {
MakeLockCmd(manifestFile string) (*exec.Cmd, error)
}

type IExecPath interface {
LookPath(file string) (string, error)
}

type ExecPath struct{}

func (_ ExecPath) LookPath(file string) (string, error) {
return exec.LookPath(file)
}

type CmdFactory struct {
execPath IExecPath
}

func (cmdf CmdFactory) MakeLockCmd(manifestFile string) (*exec.Cmd, error) {
uvPath, err := cmdf.execPath.LookPath("uv")
if err != nil {
return nil, err
}

workingDir := filepath.Dir(filepath.Clean(manifestFile))

env := os.Environ()

return &exec.Cmd{
Path: uvPath,
Args: []string{"uv", "lock"},
Dir: workingDir,
Env: env,
}, nil
}
27 changes: 27 additions & 0 deletions internal/resolution/pm/uv/cmd_factory_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package uv

import (
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
)

type execPathMock struct{}

func (execPathMock) LookPath(file string) (string, error) {
return "/usr/bin/" + file, nil
}

func TestMakeLockCmd(t *testing.T) {
factory := CmdFactory{execPath: execPathMock{}}
manifest := filepath.Join("some", "path", "pyproject.toml")

cmd, err := factory.MakeLockCmd(manifest)
assert.NoError(t, err)
assert.NotNil(t, cmd)
assert.Equal(t, "/usr/bin/uv", cmd.Path)
assert.Contains(t, cmd.Args, "uv")
assert.Contains(t, cmd.Args, "lock")
assert.Equal(t, filepath.Dir(manifest), cmd.Dir)
}
86 changes: 86 additions & 0 deletions internal/resolution/pm/uv/job.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package uv

import (
"regexp"
"strings"

"github.com/debricked/cli/internal/resolution/job"
"github.com/debricked/cli/internal/resolution/pm/util"
)

const (
executableNotFoundErrRegex = `executable file not found`
)

type Job struct {
job.BaseJob
cmdFactory ICmdFactory
}

func NewJob(file string, cmdFactory ICmdFactory) *Job {
return &Job{
BaseJob: job.NewBaseJob(file),
cmdFactory: cmdFactory,
}
}

func (j *Job) Run() {
status := "generating uv.lock"
j.SendStatus(status)

lockCmd, err := j.cmdFactory.MakeLockCmd(j.GetFile())
if err != nil {
j.handleError(j.createError(err.Error(), "", status))

return
}

if output, err := lockCmd.Output(); err != nil {
exitErr := j.GetExitError(err, string(output))
errorMessage := strings.Join([]string{string(output), exitErr.Error()}, "")
j.handleError(j.createError(errorMessage, lockCmd.String(), status))

return
}
}

func (j *Job) createError(errorStr string, cmd string, status string) job.IError {
cmdError := util.NewPMJobError(errorStr)
cmdError.SetCommand(cmd)
cmdError.SetStatus(status)

return cmdError
}

func (j *Job) handleError(cmdError job.IError) {
expressions := []string{
executableNotFoundErrRegex,
}

for _, expression := range expressions {
regex := regexp.MustCompile(expression)
matches := regex.FindAllStringSubmatch(cmdError.Error(), -1)

if len(matches) > 0 {
cmdError = j.addDocumentation(expression, matches, cmdError)
j.Errors().Append(cmdError)

return
}
}

j.Errors().Append(cmdError)
}

func (j *Job) addDocumentation(expr string, _ [][]string, cmdError job.IError) job.IError {
documentation := cmdError.Documentation()

switch expr {
case executableNotFoundErrRegex:
documentation = j.GetExecutableNotFoundErrorDocumentation("uv")
}

cmdError.SetDocumentation(documentation)

return cmdError
}
Loading
Loading