@@ -5,18 +5,28 @@ import (
55 "bytes"
66 "errors"
77 "fmt"
8+ "os"
9+ "os/exec"
810 "path/filepath"
911 "sort"
1012 "strings"
1113
14+ "github.com/langgenius/dify-plugin-daemon/pkg/entities/constants"
1215 "github.com/langgenius/dify-plugin-daemon/pkg/plugin_packager/decoder"
1316)
1417
18+ const generatedRequirementsFilename = "requirements.txt"
19+
1520type Packager struct {
1621 decoder decoder.PluginDecoder
1722 manifest string // manifest file path
1823}
1924
25+ type packageFile struct {
26+ Path string
27+ Content []byte
28+ }
29+
2030func NewPackager (decoder decoder.PluginDecoder ) * Packager {
2131 return & Packager {
2232 decoder : decoder ,
@@ -30,66 +40,197 @@ func (p *Packager) Pack(maxSize int64) ([]byte, error) {
3040 return nil , err
3141 }
3242
43+ filesToPackage , err := p .collectFiles ()
44+ if err != nil {
45+ return nil , err
46+ }
47+
3348 zipBuffer := new (bytes.Buffer )
3449 zipWriter := zip .NewWriter (zipBuffer )
3550
3651 totalSize := int64 (0 )
37-
3852 var files []FileInfoWithPath
3953
40- err = p .decoder .Walk (func (filename , dir string ) error {
41- fullPath := filepath .Join (dir , filename )
42- file , err := p .decoder .ReadFile (fullPath )
54+ for _ , file := range filesToPackage {
55+ totalSize , files , err = writePackagedFile (zipWriter , file .Path , file .Content , totalSize , maxSize , files )
4356 if err != nil {
44- return err
45- }
46- fileSize := int64 (len (file ))
47- files = append (files , FileInfoWithPath {Path : fullPath , Size : fileSize })
48- totalSize += fileSize
49- if totalSize > maxSize {
50- sort .Slice (files , func (i , j int ) bool {
51- return files [i ].Size > files [j ].Size
52- })
53- fileTop5Info := ""
54- top := 5
55- if len (files ) < 5 {
56- top = len (files )
57- }
58- for i := 0 ; i < top ; i ++ {
59- fileTop5Info += fmt .Sprintf ("%d. name: %s, size: %d bytes\n " , i + 1 , files [i ].Path , files [i ].Size )
60- }
61- errMsg := fmt .Sprintf ("Plugin package size is too large. Please ensure the uncompressed size is less than %d bytes.\n Packaged file info:\n %s" ,
62- maxSize , fileTop5Info )
63- return errors .New (errMsg )
57+ return nil , err
6458 }
59+ }
6560
66- // ISSUES: Windows path separator is \, but zip requires /, to avoid this we just simply replace all \ with / for now
67- // TODO: find a better solution
68- fullPath = strings .ReplaceAll (fullPath , "\\ " , "/" )
61+ err = zipWriter .Close ()
62+ if err != nil {
63+ return nil , err
64+ }
6965
70- zipFile , err := zipWriter .Create (fullPath )
71- if err != nil {
72- return err
73- }
66+ return zipBuffer .Bytes (), nil
67+ }
68+
69+ func (p * Packager ) collectFiles () ([]packageFile , error ) {
70+ files := make ([]packageFile , 0 )
71+ existingPaths := make (map [string ]struct {})
7472
75- _ , err = zipFile .Write (file )
73+ err := p .decoder .Walk (func (filename , dir string ) error {
74+ fullPath := filepath .Join (dir , filename )
75+ content , err := p .decoder .ReadFile (fullPath )
7676 if err != nil {
7777 return err
7878 }
7979
80+ files = append (files , packageFile {
81+ Path : fullPath ,
82+ Content : content ,
83+ })
84+ existingPaths [fullPath ] = struct {}{}
8085 return nil
8186 })
87+ if err != nil {
88+ return nil , err
89+ }
8290
91+ generatedFiles , err := p .generatedFiles ()
8392 if err != nil {
8493 return nil , err
8594 }
8695
87- err = zipWriter .Close ()
96+ for _ , file := range generatedFiles {
97+ if _ , exists := existingPaths [file .Path ]; exists {
98+ continue
99+ }
100+ files = append (files , file )
101+ }
102+
103+ sort .Slice (files , func (i , j int ) bool {
104+ return files [i ].Path < files [j ].Path
105+ })
106+
107+ return files , nil
108+ }
109+
110+ func (p * Packager ) generatedFiles () ([]packageFile , error ) {
111+ manifest , err := p .fetchManifest ()
88112 if err != nil {
89113 return nil , err
90114 }
91115
92- return zipBuffer .Bytes (), nil
116+ if manifest .Meta .Runner .Language != constants .Python {
117+ return nil , nil
118+ }
119+
120+ if _ , err := p .decoder .Stat (generatedRequirementsFilename ); err == nil {
121+ return []packageFile {}, nil
122+ } else if ! os .IsNotExist (err ) {
123+ return nil , err
124+ }
125+
126+ if _ , err := p .decoder .Stat ("pyproject.toml" ); err != nil {
127+ if os .IsNotExist (err ) {
128+ return []packageFile {}, nil
129+ }
130+ return nil , err
131+ }
132+
133+ if _ , err := p .decoder .Stat ("uv.lock" ); err != nil {
134+ if os .IsNotExist (err ) {
135+ return []packageFile {}, nil
136+ }
137+ return nil , err
138+ }
139+
140+ fsDecoder , ok := p .decoder .(* decoder.FSPluginDecoder )
141+ if ! ok {
142+ return []packageFile {}, nil
143+ }
144+
145+ requirements , err := exportRequirementsFromUv (fsDecoder .Root ())
146+ if err != nil {
147+ return nil , err
148+ }
149+
150+ return []packageFile {
151+ {
152+ Path : generatedRequirementsFilename ,
153+ Content : requirements ,
154+ },
155+ }, nil
156+ }
157+
158+ func exportRequirementsFromUv (root string ) ([]byte , error ) {
159+ uvPath := os .Getenv ("UV_PATH" )
160+ if uvPath == "" {
161+ var err error
162+ uvPath , err = exec .LookPath ("uv" )
163+ if err != nil {
164+ return nil , fmt .Errorf ("failed to find uv executable for exporting requirements.txt: %w" , err )
165+ }
166+ }
167+
168+ args := []string {
169+ "export" ,
170+ "--frozen" ,
171+ "--format" , "requirements.txt" ,
172+ "--no-group" , "dev" ,
173+ "--no-emit-project" ,
174+ "--no-hashes" ,
175+ "--no-header" ,
176+ "--no-annotate" ,
177+ }
178+
179+ cmd := exec .Command (uvPath , args ... )
180+ cmd .Dir = root
181+
182+ output , err := cmd .Output ()
183+ if err != nil {
184+ if exitErr := new (exec.ExitError ); errors .As (err , & exitErr ) {
185+ return nil , fmt .Errorf ("uv export failed: %s" , strings .TrimSpace (string (exitErr .Stderr )))
186+ }
187+ return nil , fmt .Errorf ("uv export failed: %w" , err )
188+ }
189+
190+ return output , nil
191+ }
192+
193+ func writePackagedFile (
194+ zipWriter * zip.Writer ,
195+ fullPath string ,
196+ file []byte ,
197+ totalSize int64 ,
198+ maxSize int64 ,
199+ files []FileInfoWithPath ,
200+ ) (int64 , []FileInfoWithPath , error ) {
201+ fileSize := int64 (len (file ))
202+ files = append (files , FileInfoWithPath {Path : fullPath , Size : fileSize })
203+ totalSize += fileSize
204+ if totalSize > maxSize {
205+ sort .Slice (files , func (i , j int ) bool {
206+ return files [i ].Size > files [j ].Size
207+ })
208+ fileTop5Info := ""
209+ top := 5
210+ if len (files ) < 5 {
211+ top = len (files )
212+ }
213+ for i := 0 ; i < top ; i ++ {
214+ fileTop5Info += fmt .Sprintf ("%d. name: %s, size: %d bytes\n " , i + 1 , files [i ].Path , files [i ].Size )
215+ }
216+ errMsg := fmt .Sprintf ("Plugin package size is too large. Please ensure the uncompressed size is less than %d bytes.\n Packaged file info:\n %s" ,
217+ maxSize , fileTop5Info )
218+ return totalSize , files , errors .New (errMsg )
219+ }
220+
221+ fullPath = strings .ReplaceAll (fullPath , "\\ " , "/" )
222+
223+ zipFile , err := zipWriter .Create (fullPath )
224+ if err != nil {
225+ return totalSize , files , err
226+ }
227+
228+ _ , err = zipFile .Write (file )
229+ if err != nil {
230+ return totalSize , files , err
231+ }
232+
233+ return totalSize , files , nil
93234}
94235
95236type FileInfoWithPath struct {
0 commit comments