Files
sointu/cmd/sointu-compile/main.go
T
[email protected] 06b1c86c3a fix(vm/compiler)!: don't define SU_BUFFER_LENGTH when speed is used
BREAKING CHANGE: If song uses a speed unit, SU_LENGTH_IN_SAMPLES,
SU_BUFFER_LENGTH and SU_SYNCBUFFER_LENGTH won't be defined in the
generated header file, as we cannot know their exact values during
compile time without rendering the entire song. When using a speed
unit, the user has to take responsibility of allocating large enough
buffers.

Closes #241
2026-08-15 13:49:40 +03:00

238 lines
7.7 KiB
Go

package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"strings"
"gopkg.in/yaml.v3"
"github.com/vsariola/sointu"
"github.com/vsariola/sointu/version"
"github.com/vsariola/sointu/vm/compiler"
)
func filterExtensions(input map[string]string, extensions []string) map[string]string {
ret := map[string]string{}
for _, ext := range extensions {
extWithDot := "." + ext
if inputVal, ok := input[extWithDot]; ok {
ret[extWithDot] = inputVal
}
}
return ret
}
func main() {
safe := flag.Bool("n", false, "Never overwrite files; if file already exists and would be overwritten, give an error.")
list := flag.Bool("l", false, "Do not write files; just list files that would change instead.")
stdout := flag.Bool("s", false, "Do not write files; write to standard output instead.")
help := flag.Bool("h", false, "Show help.")
rowsync := flag.Bool("r", false, "Write the current fractional row as sync #0")
library := flag.Bool("a", false, "Compile Sointu into a library. Input files are not needed.")
jsonOut := flag.Bool("j", false, "Output the song as .json file instead of compiling.")
yamlOut := flag.Bool("y", false, "Output the song as .yml file instead of compiling.")
tmplDir := flag.String("t", "", "When compiling, use the templates in this directory instead of the standard templates.")
outPath := flag.String("o", "", "Directory or filename where to write compiled code. Extension is ignored. Directory and its parents are created if needed. By default, everything is placed in the same directory where the original song file is.")
extensionsOut := flag.String("e", "", "Output only the compiled files with these comma separated extensions. For example: h,asm")
targetArch := flag.String("arch", runtime.GOARCH, "Target architecture. Defaults to OS architecture. Possible values: 386, amd64, wasm")
output16bit := flag.Bool("i", false, "Compiled song should output 16-bit integers, instead of floats.")
targetOs := flag.String("os", runtime.GOOS, "Target OS. Defaults to current OS. Possible values: windows, darwin, linux. Anything else is assumed linuxy. Ignored when targeting wasm.")
versionFlag := flag.Bool("v", false, "Print version.")
flag.Usage = printUsage
flag.Parse()
if *versionFlag {
fmt.Println(version.VersionOrHash)
os.Exit(0)
}
if (flag.NArg() == 0 && !*library) || *help {
flag.Usage()
os.Exit(0)
}
compile := !*jsonOut && !*yamlOut // if the user gives nothing to output, then the default behaviour is to compile the file
var comp *compiler.Compiler
if compile || *library {
var err error
if *tmplDir != "" {
comp, err = compiler.NewFromTemplates(*targetOs, *targetArch, *output16bit, *rowsync, *tmplDir)
} else {
comp, err = compiler.New(*targetOs, *targetArch, *output16bit, *rowsync)
}
if err != nil {
fmt.Fprintf(os.Stderr, `error creating compiler: %v`, err)
os.Exit(1)
}
}
output := func(filename string, extension string, contents []byte) error {
if *stdout {
fmt.Print(string(contents))
return nil
}
_, name := filepath.Split(filename)
var dir string
if *outPath != "" {
// check if it's an already existing directory and the user just forgot trailing slash
if info, err := os.Stat(*outPath); err == nil && info.IsDir() {
dir = *outPath
} else {
outdir, outname := filepath.Split(*outPath)
if outdir != "" {
dir = outdir
}
if outname != "" {
name = outname
}
}
}
if dir == "" {
var err error
dir, err = os.Getwd()
if err != nil {
return fmt.Errorf("could not get working directory, specify the output directory explicitly: %v", err)
}
}
name = strings.TrimSuffix(name, filepath.Ext(name)) + extension
f := filepath.Join(dir, name)
original, err := ioutil.ReadFile(f)
if err == nil {
if bytes.Compare(original, contents) == 0 {
return nil // no need to update
}
if !*list && *safe {
return fmt.Errorf("file %v would be overwritten by compiler", f)
}
}
if *list {
fmt.Println(f)
} else {
if dir != "" {
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
return fmt.Errorf("could not create output directory %v: %v", dir, err)
}
}
err := ioutil.WriteFile(f, contents, 0644)
if err != nil {
return fmt.Errorf("could not write file %v: %v", f, err)
}
}
return nil
}
process := func(filename string) error {
inputBytes, err := ioutil.ReadFile(filename)
if err != nil {
return fmt.Errorf("could not read file %v: %v", filename, err)
}
var song sointu.Song
if errJSON := json.Unmarshal(inputBytes, &song); errJSON != nil {
if errYaml := yaml.Unmarshal(inputBytes, &song); errYaml != nil {
return fmt.Errorf("song could not be unmarshaled as a .json (%v) or .yml (%v)", errJSON, errYaml)
}
}
if song.RowsPerBeat == 0 {
song.RowsPerBeat = 4
}
if song.Score.Length == 0 {
song.Score.Length = len(song.Score.Tracks[0].Patterns)
}
var compiledPlayer map[string]string
if compile {
var err error
var warnings []string
compiledPlayer, warnings, err = comp.Song(&song)
if err != nil {
return fmt.Errorf("compiling player failed: %v", err)
}
for _, warning := range warnings {
fmt.Fprintf(os.Stderr, "warning: %v\n", warning)
}
if len(*extensionsOut) > 0 {
compiledPlayer = filterExtensions(compiledPlayer, strings.Split(*extensionsOut, ","))
}
for extension, code := range compiledPlayer {
if err := output(filename, extension, []byte(code)); err != nil {
return fmt.Errorf("error outputting %v file: %v", extension, err)
}
}
}
if *jsonOut {
jsonSong, err := json.Marshal(song)
if err != nil {
return fmt.Errorf("could not marshal the song as json file: %v", err)
}
if err := output(filename, ".json", jsonSong); err != nil {
return fmt.Errorf("error outputting json file: %v", err)
}
}
if *yamlOut {
yamlSong, err := yaml.Marshal(song)
if err != nil {
return fmt.Errorf("could not marshal the song as yaml file: %v", err)
}
if err := output(filename, ".yml", yamlSong); err != nil {
return fmt.Errorf("error outputting yaml file: %v", err)
}
}
return nil
}
retval := 0
if *library {
compiledLibrary, err := comp.Library()
if err != nil {
fmt.Fprintf(os.Stderr, "compiling library failed: %v\n", err)
retval = 1
} else {
if len(*extensionsOut) > 0 {
compiledLibrary = filterExtensions(compiledLibrary, strings.Split(*extensionsOut, ","))
}
for extension, code := range compiledLibrary {
if err := output("sointu", extension, []byte(code)); err != nil {
fmt.Fprintf(os.Stderr, "error outputting %v file: %v", extension, err)
retval = 1
}
}
}
}
for _, param := range flag.Args() {
if info, err := os.Stat(param); err == nil && info.IsDir() {
jsonfiles, err := filepath.Glob(filepath.Join(param, "*.json"))
if err != nil {
fmt.Fprintf(os.Stderr, "could not glob the path %v for json files: %v\n", param, err)
retval = 1
continue
}
ymlfiles, err := filepath.Glob(filepath.Join(param, "*.yml"))
if err != nil {
fmt.Fprintf(os.Stderr, "could not glob the path %v for yml files: %v\n", param, err)
retval = 1
continue
}
files := append(ymlfiles, jsonfiles...)
for _, file := range files {
err := process(file)
if err != nil {
fmt.Fprintf(os.Stderr, "could not process file %v: %v\n", file, err)
retval = 1
}
}
} else {
err := process(param)
if err != nil {
fmt.Fprintf(os.Stderr, "could not process file %v: %v\n", param, err)
retval = 1
}
}
}
os.Exit(retval)
}
func printUsage() {
fmt.Fprintf(os.Stderr, "Sointu compiler. Input .yml or .json songs, outputs compiled songs (e.g. .asm and .h files).\nUsage: %s [flags] [path ...]\n", os.Args[0])
flag.PrintDefaults()
}