251 lines
5.4 KiB
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
|
|
}
|