diff --git a/cmd/miqt-rcc/README.md b/cmd/miqt-rcc/README.md new file mode 100644 index 00000000..b11cdc87 --- /dev/null +++ b/cmd/miqt-rcc/README.md @@ -0,0 +1,24 @@ +# miqt-rcc + +The miqt-rcc program generates the necessary wrappers to use a Qt Designer `.qrc` +resource pack in MIQT `.go` files. + +## Usage + +``` +Usage of ./miqt-rcc: + -Input string + Path to .qrc input file + -OutputGo string + (Optional) Path to .go output file. If omitted, interred from the input file path + -OutputRcc string + (Optional) Path to .rcc output file. If omitted, inferred from the output Go file path + -Package string + Package to use in generated Go files (default "main") + -Qt6 + Use Qt 6 instead of Qt 5 + -RccBinary string + (Optional) Custom path to the Qt rcc program (default "rcc") + -VariableName string + Temporary global variable name for loading embedded data (default "_resourceRcc") +``` diff --git a/cmd/miqt-rcc/integration_test.go b/cmd/miqt-rcc/integration_test.go new file mode 100644 index 00000000..48225883 --- /dev/null +++ b/cmd/miqt-rcc/integration_test.go @@ -0,0 +1,168 @@ +package main + +import ( + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "testing" +) + +func TestRcc(t *testing.T) { + + // Temporary directory + + td := t.TempDir() // The go test package auto cleans this up + + // Simulate working out of a Go module that has this miqt checkout as a + // local dependency + + RccDir, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + + fakeGoMod := `module miqt-rcc-integration-test + +go 1.19 + +require ( + github.com/mappu/miqt v1.0.0 +) + +replace github.com/mappu/miqt => ` + filepath.Clean(RccDir+`/../../`) + ` +` + err = ioutil.WriteFile(filepath.Join(td, `go.mod`), []byte(fakeGoMod), 0644) + if err != nil { + t.Fatal(err) + } + + // Add some test content + + err = ioutil.WriteFile(filepath.Join(td, "sample.data"), []byte("the quick brown fox jumps over the lazy dog"), 0644) + if err != nil { + t.Fatal(err) + } + + // Make a .qrc that includes this file + + err = ioutil.WriteFile(filepath.Join(td, "resources.qrc"), []byte( + ` + + sample.data + +`), 0644) + if err != nil { + t.Fatal(err) + } + + // Build miqt-rcc and add it to our fake tools path + + toolsDir := t.TempDir() + newPathEnv := "PATH=" + toolsDir + `:` + os.Getenv(`PATH`) + if runtime.GOOS == "windows" { // uses a different separator + newPathEnv = "PATH=" + toolsDir + `;` + os.Getenv(`PATH`) + } + + t.Run("Compile miqt-rcc", func(t *testing.T) { + + buildCmd := exec.Command(`go`, `build`, `-o`, filepath.Join(toolsDir, `miqt-rcc`)) + buildCmd.Stderr = os.Stderr + buildCmd.Stdout = os.Stdout + err = buildCmd.Run() + if err != nil { + t.Fatal(err) + } + + }) + + t.Run("Initial build", func(t *testing.T) { + + // Run miqt-rcc + + rccCmd := exec.Command(filepath.Join(toolsDir, `miqt-rcc`), `-Input`, filepath.Join(td, `resources.qrc`), `-OutputGo`, filepath.Join(td, `resources.go`)) + rccCmd.Stderr = os.Stderr + err = rccCmd.Run() + if err != nil { + t.Fatal(err) + } + + // Verify that files are created + + _, err = os.Stat(filepath.Join(td, `resources.rcc`)) + if err != nil { + t.Errorf("resources.rcc should exist: %v", err) + } + + goResult, err := ioutil.ReadFile(filepath.Join(td, `resources.go`)) + if err != nil { + t.Fatal(err) + } + + // Verify the go:embed line accurately used a relative path + + if !strings.Contains(string(goResult), "//go:embed resources.rcc\n") { + t.Fatal("missing expected embed line") + } + + // Verify the go:generate line matches our expectation + + expect := `//go:generate miqt-rcc -Input "` + filepath.Join(td, `resources.qrc`) + `" -OutputGo "resources.go" -OutputRcc "resources.rcc"` + if !strings.Contains(string(goResult), expect) { + t.Fatal("missing expected generate line") + } + + }) + + t.Run("Go generate", func(t *testing.T) { + + // Check timestamp before generation + + fiBefore, err := os.Stat(filepath.Join(td, `resources.go`)) + if err != nil { + t.Fatal(err) + } + + // Verify that `go generate` works + + regenCmd := exec.Command(`go`, `generate`) + regenCmd.Env = []string{newPathEnv} + regenCmd.Dir = td + regenCmd.Stderr = os.Stderr + err = regenCmd.Run() + if err != nil { + t.Fatal(err) + } + + goResult, err := ioutil.ReadFile(filepath.Join(td, `resources.go`)) + if err != nil { + t.Fatal(err) + } + + // Verify that the resources.go file was actually replaced + + fiAfter, err := os.Stat(filepath.Join(td, `resources.go`)) + if err != nil { + t.Fatal(err) + } + + if !fiAfter.ModTime().After(fiBefore.ModTime()) { + t.Errorf("expected mtime %v to be after original mtime %v", fiAfter.ModTime(), fiBefore.ModTime()) + } + + // Verify the go:embed line accurately used a relative path + + if !strings.Contains(string(goResult), "//go:embed resources.rcc\n") { + t.Fatal("missing expected embed line") + } + + // Verify the go:generate line matches our expectation + + expect := `//go:generate miqt-rcc -Input "` + filepath.Join(td, `resources.qrc`) + `" -OutputGo "resources.go" -OutputRcc "resources.rcc"` + if !strings.Contains(string(goResult), expect) { + t.Fatal("missing expected generate line") + } + }) +} diff --git a/cmd/miqt-rcc/main.go b/cmd/miqt-rcc/main.go index c9fe8ffe..7e35f5ce 100644 --- a/cmd/miqt-rcc/main.go +++ b/cmd/miqt-rcc/main.go @@ -9,61 +9,104 @@ import ( "io/ioutil" "os" "os/exec" + "path/filepath" + "strconv" "strings" ) -func main() { +const ( + DefaultPackageName string = "main" + DefaultVariableName string = "_resourceRcc" + DefaultIsQt6 bool = false + DefaultRccBinary string = "rcc" +) + +func RccExec() error { // Parse arguments input := flag.String("Input", "", "Path to .qrc input file") - outputRcc := flag.String("OutputRcc", "", "(Optional) Path to .rcc output file. If omitted, inferred from the input file path") outputGo := flag.String("OutputGo", "", "(Optional) Path to .go output file. If omitted, interred from the input file path") - packageName := flag.String("Package", "main", "Package to use in generated Go files") - variableName := flag.String("VariableName", "_resourceRcc", "Temporary global variable name for loading embedded data") + outputRcc := flag.String("OutputRcc", "", "(Optional) Path to .rcc output file. If omitted, inferred from the output Go file path") + packageName := flag.String("Package", DefaultPackageName, "Package to use in generated Go files") + variableName := flag.String("VariableName", DefaultVariableName, "Temporary global variable name for loading embedded data") + useQt6 := flag.Bool("Qt6", DefaultIsQt6, "Use Qt 6 instead of Qt 5") + rccBinary := flag.String("RccBinary", DefaultRccBinary, "(Optional) Custom path to the Qt rcc program") flag.Parse() // Check if input file exists if _, err := os.Stat(*input); os.IsNotExist(err) { - fmt.Fprintf(os.Stderr, "Input file '%s' not found\n", *input) - os.Exit(1) + return fmt.Errorf("Input file '%s' not found\n", *input) } // Fill in default output names, if not specified - if *outputRcc == "" { - *outputRcc = strings.TrimSuffix(*input, `.qrc`) + `.rcc` - } if *outputGo == "" { *outputGo = strings.TrimSuffix(*input, `.qrc`) + `.go` } + if *outputRcc == "" { + // Base this on the outputGo filename, not the input filename + *outputRcc = strings.TrimSuffix(*outputGo, `.go`) + `.rcc` + } + + // The rcc output path must be relative to the output go file path + + embedPath, err := filepath.Rel(filepath.Dir(*outputGo), *outputRcc) + if err != nil { + return err + } // Compile qrc to binary resource file - rccCmd := exec.Command(`rcc`, `--binary`, `-o`, *outputRcc, *input) + rccCmd := exec.Command(*rccBinary, `--binary`, `-o`, *outputRcc, *input) rccCmd.Stderr = os.Stderr rccCmd.Stdout = os.Stdout - err := rccCmd.Run() + err = rccCmd.Run() if err != nil { - fmt.Fprintf(os.Stderr, "rcc: %s\n", err.Error()) - os.Exit(1) + return err } - // Create Go file that loads the resource + // Figure out import statement + + miqtImport := `"github.com/mappu/miqt/qt"` + if *useQt6 { + miqtImport = `qt "github.com/mappu/miqt/qt6"` + } + + // Figure out regeneration command + + generate := `miqt-rcc` + + ` -Input ` + strconv.Quote(*input) + + ` -OutputGo ` + strconv.Quote(filepath.Base(*outputGo)) + + ` -OutputRcc ` + strconv.Quote(embedPath) + if *packageName != DefaultPackageName { + generate += ` -Package ` + strconv.Quote(*packageName) + } + if *variableName != DefaultVariableName { + generate += ` -Variable ` + strconv.Quote(*variableName) + } + if *useQt6 != DefaultIsQt6 { + generate += ` -Qt6` + } + if *rccBinary != DefaultRccBinary { + generate += ` -RccBinary ` + strconv.Quote(*rccBinary) + } + + // Create Go file that loads the resource goSrcData := ` package ` + *packageName + ` -//go:generate miqt-rcc "` + strings.Join(os.Args[1:], `" "`) + `" +//go:generate ` + generate + ` import ( "embed" - "github.com/mappu/miqt/qt" + ` + miqtImport + ` ) -//go:embed ` + *outputRcc + ` +//go:embed ` + embedPath + ` var ` + *variableName + ` []byte func init() { @@ -75,12 +118,21 @@ func init() { outputData, err := format.Source([]byte(goSrcData)) if err != nil { - panic(err) + return err } err = ioutil.WriteFile(*outputGo, outputData, 0644) if err != nil { - fmt.Fprintf(os.Stderr, "Writing to '%s': %s\n", *outputGo, err.Error()) + return fmt.Errorf("Writing to '%s': %w", *outputGo, err) + } + + return nil +} + +func main() { + err := RccExec() + if err != nil { + fmt.Fprintf(os.Stderr, "rcc: %s\n", err.Error()) os.Exit(1) } diff --git a/cmd/miqt-uic/README.md b/cmd/miqt-uic/README.md index 5a80143d..427de7a3 100644 --- a/cmd/miqt-uic/README.md +++ b/cmd/miqt-uic/README.md @@ -2,7 +2,21 @@ The miqt-uic program compiles Qt Designer `.ui` files into MIQT `.go` files. -For usage information, see the `examples/uidesigner` folder. +## Usage + +For example usage information, see the `examples/uidesigner` folder. + +``` +Usage of ./miqt-uic: + -InFile string + Input .ui file + -OutFile string + Output .go file, or - for stdout (default "-") + -Package string + Custom package name (default "main") + -Qt6 + Use Qt 6 instead of Qt 5 +``` ## Architecture design diff --git a/cmd/miqt-uic/main.go b/cmd/miqt-uic/main.go index 6059ba46..a683b15f 100644 --- a/cmd/miqt-uic/main.go +++ b/cmd/miqt-uic/main.go @@ -9,9 +9,14 @@ import ( "strings" ) +const ( + DefaultIsQt6 = false +) + func main() { inFile := flag.String("InFile", "", "Input .ui file") outFile := flag.String("OutFile", "-", "Output .go file, or - for stdout") + useQt6 := flag.Bool("Qt6", DefaultIsQt6, "Use Qt 6 instead of Qt 5") packageName := flag.String("Package", "main", "Custom package name") flag.Parse() @@ -31,7 +36,7 @@ func main() { panic(err) } - gosrc, err := generate(*packageName, strings.Join(os.Args[1:], " "), parsed) + gosrc, err := generate(*packageName, strings.Join(os.Args[1:], " "), parsed, *useQt6) if err != nil { panic(err) } diff --git a/cmd/miqt-uic/ui2go.go b/cmd/miqt-uic/ui2go.go index 1473b451..69089e93 100644 --- a/cmd/miqt-uic/ui2go.go +++ b/cmd/miqt-uic/ui2go.go @@ -490,7 +490,7 @@ func generateWidget(w UiWidget, parentName string, parentClass string) (string, return ret.String(), nil } -func generate(packageName string, goGenerateArgs string, u UiFile) ([]byte, error) { +func generate(packageName string, goGenerateArgs string, u UiFile, useQt6 bool) ([]byte, error) { ret := strings.Builder{} @@ -507,6 +507,11 @@ func generate(packageName string, goGenerateArgs string, u UiFile) ([]byte, erro // Header + importPackage := `"github.com/mappu/miqt/qt"` + if useQt6 { + importPackage = `qt "github.com/mappu/miqt/qt6"` + } + ret.WriteString(`// Generated by miqt-uic. To update this file, edit the .ui file in // Qt Designer, and then run 'go generate'. // @@ -515,7 +520,7 @@ func generate(packageName string, goGenerateArgs string, u UiFile) ([]byte, erro package ` + packageName + ` import ( - "github.com/mappu/miqt/qt" + ` + importPackage + ` ) type ` + u.Class + `Ui struct {