-
Notifications
You must be signed in to change notification settings - Fork 79
Package subfolders #249
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Package subfolders #249
Changes from all commits
94d5fe1
5041450
7df30ba
91ca308
2867491
f39c0e6
0d8e66a
38faf8c
eea47c1
f9cdf1c
f5eb167
03a367f
8117b06
66cbf1c
bfa9860
5923794
59bb314
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -106,6 +106,18 @@ func newPackagedCmd() *cobra.Command { | |||||
| } | ||||||
| opts.licensePaths[i] = filepath.Clean(l) | ||||||
| } | ||||||
|
|
||||||
| // Validate dir-tar paths are relative (not absolute) | ||||||
| for _, dirPath := range opts.dirTarPaths { | ||||||
| if filepath.IsAbs(dirPath) { | ||||||
| return fmt.Errorf( | ||||||
| "dir-tar path must be relative, got absolute path: %s\n\n"+ | ||||||
| "See 'docker model package --help' for more information", | ||||||
| dirPath, | ||||||
| ) | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| return nil | ||||||
| }, | ||||||
| RunE: func(cmd *cobra.Command, args []string) error { | ||||||
|
|
@@ -123,6 +135,7 @@ func newPackagedCmd() *cobra.Command { | |||||
| c.Flags().StringVar(&opts.safetensorsDir, "safetensors-dir", "", "absolute path to directory containing safetensors files and config") | ||||||
| c.Flags().StringVar(&opts.chatTemplatePath, "chat-template", "", "absolute path to chat template file (must be Jinja format)") | ||||||
| c.Flags().StringArrayVarP(&opts.licensePaths, "license", "l", nil, "absolute path to a license file") | ||||||
| c.Flags().StringArrayVar(&opts.dirTarPaths, "dir-tar", nil, "relative path to directory to package as tar (can be specified multiple times)") | ||||||
ilopezluna marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
| c.Flags().StringArrayVar(&opts.dirTarPaths, "dir-tar", nil, "relative path to directory to package as tar (can be specified multiple times)") | |
| c.Flags().StringArrayVar(&opts.dirTarPaths, "dir-tar", nil, "relative path (from --safetensors-dir, or from the GGUF file's directory) to a directory to package as a tar archive; can be specified multiple times") |
Copilot
AI
Oct 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] This re-enforces the absolute-path check already performed in PreRunE. To reduce duplication, either rely on the earlier validation or extract a shared helper used by both call sites.
Copilot
AI
Oct 17, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The manual prefix check for '..' is a bit brittle to read. Consider using strings.HasPrefix(relPath, ".."+string(filepath.Separator)) or extracting a small helper for clarity and consistency with similar validations.
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -28,6 +28,17 @@ options: | |||||
| experimentalcli: false | ||||||
| kubernetes: false | ||||||
| swarm: false | ||||||
| - option: dir-tar | ||||||
| value_type: stringArray | ||||||
| default_value: '[]' | ||||||
| description: | | ||||||
| relative path to directory to package as tar (can be specified multiple times) | ||||||
|
||||||
| relative path to directory to package as tar (can be specified multiple times) | |
| relative path (from --safetensors-dir, or from the GGUF file's directory) to a directory to package as a tar archive; can be specified multiple times |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -11,6 +11,7 @@ When packaging a Safetensors model, --safetensors-dir should point to a director | |||||
| |:--------------------|:--------------|:--------|:---------------------------------------------------------------------------------------| | ||||||
| | `--chat-template` | `string` | | absolute path to chat template file (must be Jinja format) | | ||||||
| | `--context-size` | `uint64` | `0` | context size in tokens | | ||||||
| | `--dir-tar` | `stringArray` | | relative path to directory to package as tar (can be specified multiple times) | | ||||||
|
||||||
| | `--dir-tar` | `stringArray` | | relative path to directory to package as tar (can be specified multiple times) | | |
| | `--dir-tar` | `stringArray` | | relative (to --safetensors-dir, or to the directory containing --gguf) path to a directory to package as a tar archive; can be specified multiple times | |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -102,6 +102,18 @@ func (b *Builder) WithConfigArchive(path string) (*Builder, error) { | |
| }, nil | ||
| } | ||
|
|
||
| // WithDirTar adds a directory tar archive to the artifact. | ||
| // Multiple directory tar archives can be added by calling this method multiple times. | ||
| func (b *Builder) WithDirTar(path string) (*Builder, error) { | ||
| dirTarLayer, err := partial.NewLayer(path, types.MediaTypeDirTar) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("dir tar layer from %q: %w", path, err) | ||
| } | ||
| return &Builder{ | ||
| model: mutate.AppendLayers(b.model, dirTarLayer), | ||
| }, nil | ||
| } | ||
|
|
||
|
Comment on lines
+105
to
+116
|
||
| // Target represents a build target | ||
| type Target interface { | ||
| Write(context.Context, types.ModelArtifact, io.Writer) error | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| package packaging | ||
|
|
||
| import ( | ||
| "archive/tar" | ||
| "fmt" | ||
| "io" | ||
| "os" | ||
| "path/filepath" | ||
| ) | ||
|
|
||
| // CreateDirectoryTarArchive creates a temporary tar archive containing the specified directory | ||
| // with its structure preserved. Symlinks encountered in the directory are skipped and will not be included | ||
| // in the archive. It returns the path to the temporary tar file and any error encountered. | ||
| // The caller is responsible for removing the temporary file when done. | ||
| func CreateDirectoryTarArchive(dirPath string) (string, error) { | ||
| // Verify directory exists | ||
| info, err := os.Stat(dirPath) | ||
| if err != nil { | ||
| return "", fmt.Errorf("stat directory: %w", err) | ||
| } | ||
| if !info.IsDir() { | ||
| return "", fmt.Errorf("path is not a directory: %s", dirPath) | ||
| } | ||
|
|
||
| // Create temp file | ||
| tmpFile, err := os.CreateTemp("", "dir-tar-*.tar") | ||
| if err != nil { | ||
| return "", fmt.Errorf("create temp file: %w", err) | ||
| } | ||
| tmpPath := tmpFile.Name() | ||
|
|
||
| // Track success to determine if we should clean up the temp file | ||
| shouldKeepTempFile := false | ||
| defer func() { | ||
| if !shouldKeepTempFile { | ||
| os.Remove(tmpPath) | ||
| } | ||
| }() | ||
|
|
||
| // Create tar writer | ||
| tw := tar.NewWriter(tmpFile) | ||
|
|
||
| // Walk the directory tree | ||
| err = filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error { | ||
ilopezluna marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if err != nil { | ||
| return err | ||
| } | ||
| if info == nil { | ||
| return fmt.Errorf("nil FileInfo for path: %s", path) | ||
| } | ||
| // Skip symlinks - they're not needed for model distribution and are | ||
| // skipped during extraction for security reasons | ||
| if info.Mode()&os.ModeSymlink != 0 { | ||
| return nil | ||
| } | ||
|
|
||
| // Create tar header | ||
| header, err := tar.FileInfoHeader(info, "") | ||
| if err != nil { | ||
| return fmt.Errorf("create tar header for %s: %w", path, err) | ||
| } | ||
|
|
||
| // Compute relative path from the parent of dirPath | ||
| relPath, err := filepath.Rel(filepath.Dir(dirPath), path) | ||
| if err != nil { | ||
| return fmt.Errorf("compute relative path: %w", err) | ||
| } | ||
|
|
||
| // Use forward slashes for tar archive paths | ||
| header.Name = filepath.ToSlash(relPath) | ||
|
|
||
| // Write header | ||
| if err := tw.WriteHeader(header); err != nil { | ||
| return fmt.Errorf("write tar header: %w", err) | ||
| } | ||
|
|
||
| // If it's a file, write its contents | ||
| if !info.IsDir() { | ||
| file, err := os.Open(path) | ||
sourcery-ai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if err != nil { | ||
| return fmt.Errorf("open file %s: %w", path, err) | ||
| } | ||
|
|
||
| // Copy file contents | ||
| if _, err := io.Copy(tw, file); err != nil { | ||
| file.Close() | ||
| return fmt.Errorf("write tar content for %s: %w", path, err) | ||
| } | ||
| if err := file.Close(); err != nil { | ||
| return fmt.Errorf("close file %s: %w", path, err) | ||
| } | ||
| } | ||
|
|
||
| return nil | ||
| }) | ||
|
|
||
| if err != nil { | ||
| tw.Close() | ||
| tmpFile.Close() | ||
| return "", fmt.Errorf("walk directory: %w", err) | ||
| } | ||
|
|
||
| // Close tar writer | ||
| if err := tw.Close(); err != nil { | ||
| tmpFile.Close() | ||
| return "", fmt.Errorf("close tar writer: %w", err) | ||
| } | ||
|
|
||
| // Close temp file | ||
| if err := tmpFile.Close(); err != nil { | ||
| return "", fmt.Errorf("close temp file: %w", err) | ||
| } | ||
|
|
||
| shouldKeepTempFile = true | ||
| return tmpPath, nil | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] Absolute-path validation for --dir-tar is duplicated here and again during processing. Consider centralizing this check (e.g., in PreRunE only or a small helper) to avoid drift and keep validation in one place.