Files
MosisService/portal/cmd/mosis/cmd/build.go

251 lines
5.4 KiB
Go

package cmd
import (
"archive/zip"
"bufio"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra"
"omixlab.com/mosis-portal/pkg/mospkg"
)
// BuildCmd returns the build command
func BuildCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "build [directory]",
Short: "Create .mosis package",
Long: "Build a Mosis app package from the project directory.",
Args: cobra.MaximumNArgs(1),
RunE: runBuild,
}
cmd.Flags().StringP("output", "o", "", "Output path (default: dist/<package-id>-<version>.mosis)")
cmd.Flags().Bool("no-compress", false, "Skip compression")
return cmd
}
func runBuild(cmd *cobra.Command, args []string) error {
dir := "."
if len(args) > 0 {
dir = args[0]
}
// Read manifest
fmt.Println("Reading manifest.json...")
manifestPath := filepath.Join(dir, "manifest.json")
manifestData, err := os.ReadFile(manifestPath)
if err != nil {
return fmt.Errorf("manifest.json not found\n\nAre you in a Mosis project directory?")
}
manifest, err := mospkg.ParseManifest(manifestData)
if err != nil {
return fmt.Errorf("invalid manifest.json: %w", err)
}
// Validate manifest
validationErrs := manifest.Validate()
if len(validationErrs) > 0 {
fmt.Println("Manifest validation failed:")
for _, e := range validationErrs {
fmt.Printf(" - %s\n", e.Message)
}
return fmt.Errorf("fix manifest errors before building")
}
fmt.Printf("Package: %s v%s (%d)\n", manifest.ID, manifest.Version, manifest.VersionCode)
// Determine output path
output, _ := cmd.Flags().GetString("output")
if output == "" {
output = filepath.Join(dir, "dist", fmt.Sprintf("%s-%s.mosis", manifest.ID, manifest.Version))
}
// Ensure dist directory exists
if err := os.MkdirAll(filepath.Dir(output), 0755); err != nil {
return fmt.Errorf("create output directory: %w", err)
}
// Load .mosisignore patterns
ignorePatterns := loadIgnorePatterns(dir)
// Collect files
fmt.Println("\nCollecting files...")
files, err := collectFiles(dir, ignorePatterns)
if err != nil {
return fmt.Errorf("collect files: %w", err)
}
for _, f := range files {
fmt.Printf("✓ %s\n", f)
}
// Create package
fmt.Println("\nCreating package...")
noCompress, _ := cmd.Flags().GetBool("no-compress")
if err := createPackage(dir, files, output, !noCompress); err != nil {
return fmt.Errorf("create package: %w", err)
}
info, _ := os.Stat(output)
fmt.Printf("✓ Package created: %s (%.1f KB)\n", output, float64(info.Size())/1024)
fmt.Println("\n⚠ Package is unsigned. Run 'mosis sign' before publishing.")
return nil
}
func loadIgnorePatterns(dir string) []string {
patterns := []string{
".git",
".git/**",
".gitignore",
"dist",
"dist/**",
".mosisignore",
"*.md",
}
ignorePath := filepath.Join(dir, ".mosisignore")
f, err := os.Open(ignorePath)
if err != nil {
return patterns
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
patterns = append(patterns, line)
}
return patterns
}
func collectFiles(dir string, ignorePatterns []string) ([]string, error) {
var files []string
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// Get relative path
relPath, _ := filepath.Rel(dir, path)
if relPath == "." {
return nil
}
// Convert to forward slashes for consistency
relPath = filepath.ToSlash(relPath)
// Check ignore patterns
for _, pattern := range ignorePatterns {
pattern = strings.TrimSuffix(pattern, "/")
// Exact match
if relPath == pattern {
if info.IsDir() {
return filepath.SkipDir
}
return nil
}
// Directory prefix match
if strings.HasPrefix(relPath, pattern+"/") {
if info.IsDir() {
return filepath.SkipDir
}
return nil
}
// Glob pattern with **
if strings.Contains(pattern, "**") {
base := strings.Split(pattern, "**")[0]
if strings.HasPrefix(relPath, base) {
if info.IsDir() {
return filepath.SkipDir
}
return nil
}
}
// Wildcard extension match
if strings.HasPrefix(pattern, "*.") {
ext := strings.TrimPrefix(pattern, "*")
if strings.HasSuffix(relPath, ext) {
return nil
}
}
}
if !info.IsDir() {
files = append(files, relPath)
}
return nil
})
return files, err
}
func createPackage(dir string, files []string, output string, compress bool) error {
outFile, err := os.Create(output)
if err != nil {
return err
}
defer outFile.Close()
writer := zip.NewWriter(outFile)
defer writer.Close()
for _, relPath := range files {
fullPath := filepath.Join(dir, relPath)
info, err := os.Stat(fullPath)
if err != nil {
return fmt.Errorf("stat %s: %w", relPath, err)
}
header, err := zip.FileInfoHeader(info)
if err != nil {
return fmt.Errorf("header %s: %w", relPath, err)
}
// Use forward slashes in ZIP
header.Name = filepath.ToSlash(relPath)
if compress {
header.Method = zip.Deflate
} else {
header.Method = zip.Store
}
w, err := writer.CreateHeader(header)
if err != nil {
return fmt.Errorf("create %s: %w", relPath, err)
}
f, err := os.Open(fullPath)
if err != nil {
return fmt.Errorf("open %s: %w", relPath, err)
}
if _, err := io.Copy(w, f); err != nil {
f.Close()
return fmt.Errorf("copy %s: %w", relPath, err)
}
f.Close()
}
return nil
}