@@ -14,6 +14,35 @@ import (
1414 "github.com/docker/model-runner/pkg/distribution/types"
1515)
1616
17+ // DirectoryOptions configures the behavior of FromDirectory.
18+ type DirectoryOptions struct {
19+ // Exclusions is a list of patterns to exclude from packaging.
20+ // Patterns can be:
21+ // - Directory names (e.g., ".git", "__pycache__") - excludes the entire directory
22+ // - File names (e.g., "README.md") - excludes files with this exact name
23+ // - Glob patterns (e.g., "*.log", "*.tmp") - excludes files matching the pattern
24+ // - Paths with slashes (e.g., "logs/debug.log") - excludes specific paths
25+ Exclusions []string
26+ }
27+
28+ // DirectoryOption is a functional option for configuring FromDirectory.
29+ type DirectoryOption func (* DirectoryOptions )
30+
31+ // WithExclusions specifies patterns to exclude from packaging.
32+ // Patterns can be directory names, file names, glob patterns, or specific paths.
33+ //
34+ // Examples:
35+ //
36+ // WithExclusions(".git", "__pycache__") // Exclude directories
37+ // WithExclusions("README.md", "CHANGELOG.md") // Exclude specific files
38+ // WithExclusions("*.log", "*.tmp") // Exclude by pattern
39+ // WithExclusions("logs/", "cache/") // Exclude directories (trailing slash)
40+ func WithExclusions (patterns ... string ) DirectoryOption {
41+ return func (opts * DirectoryOptions ) {
42+ opts .Exclusions = append (opts .Exclusions , patterns ... )
43+ }
44+ }
45+
1746// FromDirectory creates a Builder from a directory containing model files.
1847// It recursively scans the directory and adds each non-hidden file as a separate layer.
1948// Each layer's filepath annotation preserves the relative path from the directory root.
@@ -29,7 +58,17 @@ import (
2958// text_encoder/
3059// config.json -> layer with annotation "text_encoder/config.json"
3160// model.safetensors -> layer with annotation "text_encoder/model.safetensors"
32- func FromDirectory (dirPath string ) (* Builder , error ) {
61+ //
62+ // Example with exclusions:
63+ //
64+ // builder.FromDirectory(dirPath, builder.WithExclusions(".git", "__pycache__", "*.log"))
65+ func FromDirectory (dirPath string , opts ... DirectoryOption ) (* Builder , error ) {
66+ // Apply options
67+ options := & DirectoryOptions {}
68+ for _ , opt := range opts {
69+ opt (options )
70+ }
71+
3372 // Verify directory exists
3473 info , err := os .Stat (dirPath )
3574 if err != nil {
@@ -63,6 +102,20 @@ func FromDirectory(dirPath string) (*Builder, error) {
63102 return nil
64103 }
65104
105+ // Calculate relative path from the directory root
106+ relPath , err := filepath .Rel (dirPath , path )
107+ if err != nil {
108+ return fmt .Errorf ("compute relative path: %w" , err )
109+ }
110+
111+ // Check exclusions
112+ if shouldExclude (info , relPath , options .Exclusions ) {
113+ if info .IsDir () {
114+ return filepath .SkipDir
115+ }
116+ return nil
117+ }
118+
66119 // Skip directories (but continue walking into them)
67120 if info .IsDir () {
68121 return nil
@@ -73,12 +126,6 @@ func FromDirectory(dirPath string) (*Builder, error) {
73126 return nil
74127 }
75128
76- // Calculate relative path from the directory root
77- relPath , err := filepath .Rel (dirPath , path )
78- if err != nil {
79- return fmt .Errorf ("compute relative path: %w" , err )
80- }
81-
82129 // Classify the file to determine media type
83130 fileType := files .Classify (path )
84131 mediaType := fileTypeToMediaType (fileType )
@@ -100,6 +147,10 @@ func FromDirectory(dirPath string) (*Builder, error) {
100147 detectedFormat = types .FormatDiffusers
101148 }
102149 weightFiles = append (weightFiles , path )
150+ case files .FileTypeUnknown :
151+ case files .FileTypeConfig :
152+ case files .FileTypeLicense :
153+ case files .FileTypeChatTemplate :
103154 }
104155
105156 // Create layer with relative path annotation
@@ -160,6 +211,71 @@ func FromDirectory(dirPath string) (*Builder, error) {
160211 }, nil
161212}
162213
214+ // shouldExclude checks if a file or directory should be excluded based on the exclusion patterns.
215+ func shouldExclude (info os.FileInfo , relPath string , exclusions []string ) bool {
216+ if len (exclusions ) == 0 {
217+ return false
218+ }
219+
220+ name := info .Name ()
221+ // Normalize path separators for cross-platform matching
222+ normalizedRelPath := filepath .ToSlash (relPath )
223+
224+ for _ , pattern := range exclusions {
225+ // Normalize the pattern
226+ pattern = filepath .ToSlash (pattern )
227+
228+ // Pattern ends with "/" - match directories only
229+ if strings .HasSuffix (pattern , "/" ) {
230+ if info .IsDir () {
231+ dirPattern := strings .TrimSuffix (pattern , "/" )
232+ // Match directory name
233+ if name == dirPattern {
234+ return true
235+ }
236+ // Match full path
237+ if normalizedRelPath == dirPattern || strings .HasPrefix (normalizedRelPath , dirPattern + "/" ) {
238+ return true
239+ }
240+ }
241+ continue
242+ }
243+
244+ // Pattern contains "/" - treat as path match
245+ if strings .Contains (pattern , "/" ) {
246+ // Exact path match
247+ if normalizedRelPath == pattern {
248+ return true
249+ }
250+ // Directory path prefix match
251+ if info .IsDir () && strings .HasPrefix (normalizedRelPath + "/" , pattern + "/" ) {
252+ return true
253+ }
254+ // File inside excluded directory
255+ if strings .HasPrefix (normalizedRelPath , pattern + "/" ) {
256+ return true
257+ }
258+ continue
259+ }
260+
261+ // Pattern contains glob characters - use glob matching
262+ if strings .ContainsAny (pattern , "*?[]" ) {
263+ matched , err := filepath .Match (pattern , name )
264+ if err == nil && matched {
265+ return true
266+ }
267+ continue
268+ }
269+
270+ // Simple name match (works for both files and directories)
271+ if name == pattern {
272+ return true
273+ }
274+ }
275+
276+ return false
277+ }
278+
163279// fileTypeToMediaType converts a FileType to the corresponding OCI MediaType
164280func fileTypeToMediaType (ft files.FileType ) oci.MediaType {
165281 switch ft {
0 commit comments