diff --git a/cmd/prepare_worker.go b/cmd/prepare_worker.go
index dca38a5..5501698 100644
--- a/cmd/prepare_worker.go
+++ b/cmd/prepare_worker.go
@@ -10,8 +10,8 @@ import (
"github.com/spf13/cobra"
"github.com/spf13/pflag"
- "github.com/temporalio/features/sdkbuild"
"github.com/temporalio/omes/cmd/cmdoptions"
+ "github.com/temporalio/omes/sdkbuild"
"go.uber.org/zap"
)
diff --git a/cmd/run_worker.go b/cmd/run_worker.go
index 8c9c7e5..a672012 100644
--- a/cmd/run_worker.go
+++ b/cmd/run_worker.go
@@ -14,9 +14,9 @@ import (
"github.com/spf13/cobra"
"github.com/spf13/pflag"
- "github.com/temporalio/features/sdkbuild"
"github.com/temporalio/omes/cmd/cmdoptions"
"github.com/temporalio/omes/loadgen"
+ "github.com/temporalio/omes/sdkbuild"
"go.temporal.io/sdk/client"
"go.temporal.io/sdk/testsuite"
)
diff --git a/go.mod b/go.mod
index 9bf8222..3513995 100644
--- a/go.mod
+++ b/go.mod
@@ -5,18 +5,18 @@ go 1.20
require (
github.com/gogo/protobuf v1.3.2
github.com/golang/protobuf v1.5.3
+ github.com/otiai10/copy v1.14.0
github.com/prometheus/client_golang v1.16.0
github.com/spf13/cobra v1.7.0
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.8.4
- github.com/temporalio/features v0.0.0-20231218231852-27c681667dae
go.temporal.io/api v1.26.1
- go.temporal.io/sdk v1.25.2-0.20231129171107-288a04f72145
+ go.temporal.io/sdk v1.25.2-0.20240109200522-5ca9a4dfd4c3
go.uber.org/zap v1.25.0
- golang.org/x/mod v0.12.0
+ golang.org/x/mod v0.14.0
golang.org/x/sync v0.5.0
golang.org/x/sys v0.15.0
- google.golang.org/protobuf v1.31.0
+ google.golang.org/protobuf v1.32.0
)
require (
@@ -36,7 +36,6 @@ require (
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
- github.com/otiai10/copy v1.14.0 // indirect
github.com/pborman/uuid v1.2.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/robfig/cron v1.2.0 // indirect
@@ -44,19 +43,13 @@ require (
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/goleak v1.2.1 // indirect
go.uber.org/multierr v1.11.0 // indirect
+ golang.org/x/exp v0.0.0-20231127185646-65229373498e // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.3.0 // indirect
- google.golang.org/genproto v0.0.0-20231127180814-3a041ad873d4 // indirect
- google.golang.org/genproto/googleapis/api v0.0.0-20231127180814-3a041ad873d4 // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4 // indirect
- google.golang.org/grpc v1.59.0 // indirect
+ google.golang.org/genproto v0.0.0-20240102182953-50ed04b92917 // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 // indirect
+ google.golang.org/grpc v1.60.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
-
-// This is dumb, but necesary because Go (for some commands) can't figure out the transitive
-// local-replace inside of the features module itself, so we have to help it.
-replace (
- github.com/temporalio/features/features => github.com/temporalio/features/features v0.0.0-20231218231852-27c681667dae
- github.com/temporalio/features/harness/go => github.com/temporalio/features/harness/go v0.0.0-20231218231852-27c681667dae
-)
diff --git a/go.sum b/go.sum
index 107fd44..cba8055 100644
--- a/go.sum
+++ b/go.sum
@@ -98,15 +98,13 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
-github.com/temporalio/features v0.0.0-20231218231852-27c681667dae h1:d5LK3X10VZEWpLhZ5dIPcirvELKVtT4rEV+8wzfgBRM=
-github.com/temporalio/features v0.0.0-20231218231852-27c681667dae/go.mod h1:Jm0Yq8DKEkSzcQ1YbZ5yeqrD6iyyWzQMcsXF0G1ylM4=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.temporal.io/api v1.26.1 h1:YqGQsOr/Tx4nVdA8wCv74AxesaIzCRHWb3KkHrYqI8k=
go.temporal.io/api v1.26.1/go.mod h1:Y/rALXTprFO+bvAlAfLFoJj7KpQIcL4GDQVN6fhYIa4=
-go.temporal.io/sdk v1.25.2-0.20231129171107-288a04f72145 h1:aV7tRpzB3tr9LGs4/SN7MSWSbVx+bgDYfOoGMjk4oEM=
-go.temporal.io/sdk v1.25.2-0.20231129171107-288a04f72145/go.mod h1:MHw8PEOVmOJC1yduTVxYq1GsM5kkQg0sIwRST7cRHoo=
+go.temporal.io/sdk v1.25.2-0.20240109200522-5ca9a4dfd4c3 h1:XzkvOc0UATBM0SC2SO/GKaXq3JkJwe2rDeRhW2u11zM=
+go.temporal.io/sdk v1.25.2-0.20240109200522-5ca9a4dfd4c3/go.mod h1:nRT6pheoo7UXrrgMh26r7t4IuJFZmu277SkaiVp/tZE=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
@@ -123,6 +121,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20231127185646-65229373498e h1:Gvh4YaCaXNs6dKTlfgismwWZKyjVZXwOPfIyUaqU3No=
+golang.org/x/exp v0.0.0-20231127185646-65229373498e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
@@ -130,8 +130,8 @@ golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHl
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
-golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
+golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -190,23 +190,23 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20231127180814-3a041ad873d4 h1:W12Pwm4urIbRdGhMEg2NM9O3TWKjNcxQhs46V0ypf/k=
-google.golang.org/genproto v0.0.0-20231127180814-3a041ad873d4/go.mod h1:5RBcpGRxr25RbDzY5w+dmaqpSEvl8Gwl1x2CICf60ic=
-google.golang.org/genproto/googleapis/api v0.0.0-20231127180814-3a041ad873d4 h1:ZcOkrmX74HbKFYnpPY8Qsw93fC29TbJXspYKaBkSXDQ=
-google.golang.org/genproto/googleapis/api v0.0.0-20231127180814-3a041ad873d4/go.mod h1:k2dtGpRrbsSyKcNPKKI5sstZkrNCZwpU/ns96JoHbGg=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4 h1:DC7wcm+i+P1rN3Ff07vL+OndGg5OhNddHyTA+ocPqYE=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20231127180814-3a041ad873d4/go.mod h1:eJVxU6o+4G1PSczBr85xmyvSNYAKvAYgkub40YGomFM=
+google.golang.org/genproto v0.0.0-20240102182953-50ed04b92917 h1:nz5NESFLZbJGPFxDT/HCn+V1mZ8JGNoY4nUpmW/Y2eg=
+google.golang.org/genproto v0.0.0-20240102182953-50ed04b92917/go.mod h1:pZqR+glSb11aJ+JQcczCvgf47+duRuzNSKqE8YAQnV0=
+google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917 h1:rcS6EyEaoCO52hQDupoSfrxI3R6C2Tq741is7X8OvnM=
+google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917/go.mod h1:CmlNWB9lSezaYELKS5Ym1r44VrrbPUa7JTvw+6MbpJ0=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 h1:6G8oQ016D88m1xAKljMlBOOGWDZkes4kMhgGFlf8WcQ=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917/go.mod h1:xtjpI3tXFPP051KaWnhvxkiubL/6dJ18vLVf7q2pTOU=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
-google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
-google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
+google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU=
+google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
-google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
+google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
diff --git a/sdkbuild/dotnet.go b/sdkbuild/dotnet.go
new file mode 100644
index 0000000..8e10137
--- /dev/null
+++ b/sdkbuild/dotnet.go
@@ -0,0 +1,144 @@
+package sdkbuild
+
+import (
+ "context"
+ "fmt"
+ "html"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+ "strings"
+)
+
+// BuildDotNetProgramOptions are options for BuildDotNetProgram.
+type BuildDotNetProgramOptions struct {
+ // Directory that will have a temporary directory created underneath.
+ BaseDir string
+ // Required version. If it contains a slash, it is assumed to be a path to the
+ // base of the repo (and will have a src/Temporalio/Temporalio.csproj child).
+ // Otherwise it is a NuGet version.
+ Version string
+ // If present, this directory is expected to exist beneath base dir. Otherwise
+ // a temporary dir is created.
+ DirName string
+ // Required Program.cs content. If not set, no Program.cs is created (so it)
+ ProgramContents string
+ // Required csproj content. This should not contain a dependency on Temporalio
+ // because this adds a package/project reference near the end.
+ CsprojContents string
+}
+
+// DotNetProgram is a .NET-specific implementation of Program.
+type DotNetProgram struct {
+ dir string
+}
+
+var _ Program = (*DotNetProgram)(nil)
+
+func BuildDotNetProgram(ctx context.Context, options BuildDotNetProgramOptions) (*DotNetProgram, error) {
+ if options.BaseDir == "" {
+ return nil, fmt.Errorf("base dir required")
+ } else if options.Version == "" {
+ return nil, fmt.Errorf("version required")
+ } else if options.ProgramContents == "" {
+ return nil, fmt.Errorf("program contents required")
+ } else if options.CsprojContents == "" {
+ return nil, fmt.Errorf("csproj contents required")
+ }
+
+ // Create temp dir if needed that we will remove if creating is unsuccessful
+ success := false
+ var dir string
+ if options.DirName != "" {
+ dir = filepath.Join(options.BaseDir, options.DirName)
+ } else {
+ var err error
+ dir, err = os.MkdirTemp(options.BaseDir, "program-")
+ if err != nil {
+ return nil, fmt.Errorf("failed making temp dir: %w", err)
+ }
+ defer func() {
+ if !success {
+ // Intentionally swallow error
+ _ = os.RemoveAll(dir)
+ }
+ }()
+ }
+
+ // Create program.csproj
+ var depLine string
+ // Slash means it is a path
+ if strings.ContainsAny(options.Version, `/\`) {
+ // Get absolute path of csproj file
+ absCsproj, err := filepath.Abs(filepath.Join(options.Version, "src/Temporalio/Temporalio.csproj"))
+ if err != nil {
+ return nil, fmt.Errorf("cannot make absolute path from version: %w", err)
+ } else if _, err := os.Stat(absCsproj); err != nil {
+ return nil, fmt.Errorf("cannot find version path of %v: %w", absCsproj, err)
+ }
+ depLine = ``
+ // Need to build this csproj first
+ cmd := exec.CommandContext(ctx, "dotnet", "build", absCsproj)
+ cmd.Dir = dir
+ cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
+ if err := cmd.Run(); err != nil {
+ return nil, fmt.Errorf("failed dotnet build of csproj in version: %w", err)
+ }
+ } else {
+ depLine = ``
+ }
+ // Add the item group for the Temporalio dep just before the ending project tag
+ endProjectTag := strings.LastIndex(options.CsprojContents, "")
+ if endProjectTag == -1 {
+ return nil, fmt.Errorf("no ending project tag found in csproj contents")
+ }
+ csproj := options.CsprojContents[:endProjectTag] + "\n \n " + depLine +
+ "\n \n" + options.CsprojContents[endProjectTag:]
+ if err := os.WriteFile(filepath.Join(dir, "program.csproj"), []byte(csproj), 0644); err != nil {
+ return nil, fmt.Errorf("failed writing program.csproj: %w", err)
+ }
+
+ // Create Program.cs
+ if err := os.WriteFile(filepath.Join(dir, "Program.cs"), []byte(options.ProgramContents), 0644); err != nil {
+ return nil, fmt.Errorf("failed writing Program.cs: %w", err)
+ }
+
+ // Build it into build folder
+ cmd := exec.CommandContext(ctx, "dotnet", "build", "--output", "build")
+ cmd.Dir = dir
+ cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
+ if err := cmd.Run(); err != nil {
+ return nil, fmt.Errorf("failed dotnet build: %w", err)
+ }
+
+ // All good
+ success = true
+ return &DotNetProgram{dir}, nil
+}
+
+// DotNetProgramFromDir recreates the Go program from a Dir() result of a
+// BuildDotNetProgram().
+func DotNetProgramFromDir(dir string) (*DotNetProgram, error) {
+ // Quick sanity check on the presence of program.csproj
+ if _, err := os.Stat(filepath.Join(dir, "program.csproj")); err != nil {
+ return nil, fmt.Errorf("failed finding program.csproj in dir: %w", err)
+ }
+ return &DotNetProgram{dir}, nil
+}
+
+// Dir is the directory to run in.
+func (d *DotNetProgram) Dir() string { return d.dir }
+
+// NewCommand makes a new command for the given args.
+func (d *DotNetProgram) NewCommand(ctx context.Context, args ...string) (*exec.Cmd, error) {
+ exe := "./build/program"
+ if runtime.GOOS == "windows" {
+ exe += ".exe"
+ }
+ cmd := exec.CommandContext(ctx, exe, args...)
+ cmd.Dir = d.dir
+ cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
+ return cmd, nil
+}
diff --git a/sdkbuild/go.go b/sdkbuild/go.go
new file mode 100644
index 0000000..e7c9874
--- /dev/null
+++ b/sdkbuild/go.go
@@ -0,0 +1,173 @@
+package sdkbuild
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+ "strings"
+)
+
+const sdkImport = "go.temporal.io/sdk"
+
+// BuildGoProgramOptions are options for BuildGoProgram.
+type BuildGoProgramOptions struct {
+ // Directory that will have a temporary directory created underneath
+ BaseDir string
+ // If not set, not put in go.mod which means go mod tidy will automatically
+ // use latest. If set and does not start with a "v", it is assumed to be a
+ // path, otherwise it is a specific version.
+ Version string
+ // The SDK Repository import to use. If unspecified we default to go.temporal.io/sdk
+ // If specified version must also be provided
+ SDKRepository string
+ // Required go.mod contents
+ GoModContents string
+ // Required main.go contents
+ GoMainContents string
+ // If present, this directory is expected to exist beneath base dir. Otherwise
+ // a temporary dir is created.
+ DirName string
+ // Optional set of tags to build with
+ GoBuildTags []string
+ // If present, applied to build commands before run. May be called multiple
+ // times for a single build.
+ ApplyToCommand func(context.Context, *exec.Cmd) error
+}
+
+// GoProgram is a Go-specific implementation of Program.
+type GoProgram struct {
+ dir string
+}
+
+var _ Program = (*GoProgram)(nil)
+
+// BuildGoProgram builds a Go program. If completed successfully, this can be
+// stored and re-obtained via GoProgramFromDir() with the Dir() value.
+func BuildGoProgram(ctx context.Context, options BuildGoProgramOptions) (*GoProgram, error) {
+ if options.BaseDir == "" {
+ return nil, fmt.Errorf("base dir required")
+ } else if options.GoModContents == "" {
+ return nil, fmt.Errorf("go.mod contents required")
+ } else if options.GoMainContents == "" {
+ return nil, fmt.Errorf("main.go contents required")
+ }
+
+ // Create temp dir if needed that we will remove if creating is unsuccessful
+ success := false
+ var dir string
+ if options.DirName != "" {
+ dir = filepath.Join(options.BaseDir, options.DirName)
+ } else {
+ var err error
+ dir, err = os.MkdirTemp(options.BaseDir, "program-")
+ if err != nil {
+ return nil, fmt.Errorf("failed making temp dir: %w", err)
+ }
+ defer func() {
+ if !success {
+ // Intentionally swallow error
+ _ = os.RemoveAll(dir)
+ }
+ }()
+ }
+
+ // Create go.mod
+ goMod := options.GoModContents
+ // If a version is specified, overwrite the SDK to use that
+ if options.Version != "" || options.SDKRepository != "" {
+ // If version does not start with a "v" we assume path unless the SDK repository is provided
+ if options.SDKRepository != "" {
+ if options.Version == "" {
+ return nil, errors.New("Version must be provided alongside SDKRepository")
+ }
+ goMod += fmt.Sprintf("\nreplace %s => %s %s", sdkImport, options.SDKRepository, options.Version)
+ } else if strings.HasPrefix(options.Version, "v") {
+ goMod += fmt.Sprintf("\nreplace %s => %s %s", sdkImport, sdkImport, options.Version)
+ } else {
+ absVersion, err := filepath.Abs(options.Version)
+ if err != nil {
+ return nil, fmt.Errorf("version does not start with 'v' and cannot get abs dir: %w", err)
+ }
+ relVersion, err := filepath.Rel(dir, absVersion)
+ if err != nil {
+ return nil, fmt.Errorf("version does not start with 'v' and unable to relativize: %w", err)
+ }
+ goMod += fmt.Sprintf("\nreplace %s => %s", sdkImport, filepath.ToSlash(relVersion))
+ }
+ }
+ if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte(goMod), 0644); err != nil {
+ return nil, fmt.Errorf("failed writing go.mod: %w", err)
+ }
+
+ // Create main.go
+ if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte(options.GoMainContents), 0644); err != nil {
+ return nil, fmt.Errorf("failed writing main.go: %w", err)
+ }
+
+ // Tidy it
+ cmd := exec.CommandContext(ctx, "go", "mod", "tidy")
+ cmd.Dir = dir
+ cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
+ if options.ApplyToCommand != nil {
+ if err := options.ApplyToCommand(ctx, cmd); err != nil {
+ return nil, err
+ }
+ }
+ if err := cmd.Run(); err != nil {
+ return nil, fmt.Errorf("failed go mod tidy: %w", err)
+ }
+
+ // Build it
+ exe := "program"
+ if runtime.GOOS == "windows" {
+ exe += ".exe"
+ }
+ cmdArgs := []string{"build", "-o", exe}
+ for _, tag := range options.GoBuildTags {
+ cmdArgs = append(cmdArgs, "-tags", tag)
+ }
+ cmd = exec.CommandContext(ctx, "go", cmdArgs...)
+ cmd.Dir = dir
+ cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
+ if options.ApplyToCommand != nil {
+ if err := options.ApplyToCommand(ctx, cmd); err != nil {
+ return nil, err
+ }
+ }
+ if err := cmd.Run(); err != nil {
+ return nil, fmt.Errorf("failed go build: %w", err)
+ }
+
+ // All good
+ success = true
+ return &GoProgram{dir}, nil
+}
+
+// GoProgramFromDir recreates the Go program from a Dir() result of a
+// BuildGoProgram().
+func GoProgramFromDir(dir string) (*GoProgram, error) {
+ // Quick sanity check on the presence of go.mod
+ if _, err := os.Stat(filepath.Join(dir, "go.mod")); err != nil {
+ return nil, fmt.Errorf("failed finding go.mod in dir: %w", err)
+ }
+ return &GoProgram{dir}, nil
+}
+
+// Dir is the directory to run in.
+func (g *GoProgram) Dir() string { return g.dir }
+
+// NewCommand makes a new command for the given args.
+func (g *GoProgram) NewCommand(ctx context.Context, args ...string) (*exec.Cmd, error) {
+ exe := "./program"
+ if runtime.GOOS == "windows" {
+ exe += ".exe"
+ }
+ cmd := exec.CommandContext(ctx, exe, args...)
+ cmd.Dir = g.dir
+ cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
+ return cmd, nil
+}
diff --git a/sdkbuild/java.go b/sdkbuild/java.go
new file mode 100644
index 0000000..d814a4f
--- /dev/null
+++ b/sdkbuild/java.go
@@ -0,0 +1,212 @@
+package sdkbuild
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+ "strings"
+
+ "github.com/otiai10/copy"
+)
+
+// BuildJavaProgramOptions are options for BuildJavaProgram.
+type BuildJavaProgramOptions struct {
+ // Directory that will have a temporary directory created underneath. This
+ // should be a gradle project with a build.gradle, a gradlew executable, etc.
+ BaseDir string
+ // If not set, not put in build.gradle which means gradle will automatically
+ // use latest. If set and contains a slash it is assumed to be a path,
+ // otherwise it is a specific version (with leading "v" is trimmed if
+ // present).
+ Version string
+ // Required Gradle "implementation" dependency name of BaseDir. This is
+ // usually "::" with each value replaced with
+ // proper values.
+ HarnessDependency string
+ // Required fully-qualified class name for main.
+ MainClass string
+ // If true, performs an eager build. This is often just to prime system-level
+ // caches and do extra validation, the build won't be used by NewCommand.
+ Build bool
+ // If present, this directory is expected to exist beneath base dir. Otherwise
+ // a temporary dir is created.
+ DirName string
+ // If present, applied to build commands before run. May be called multiple
+ // times for a single build.
+ ApplyToCommand func(context.Context, *exec.Cmd) error
+}
+
+// JavaProgram is a Java-specific implementation of Program.
+type JavaProgram struct {
+ dir string
+}
+
+var _ Program = (*JavaProgram)(nil)
+
+// BuildJavaProgram builds a Java program. If completed successfully, this can
+// be stored and re-obtained via JavaProgramFromDir() with the Dir() value (but
+// the entire BaseDir must be present too).
+func BuildJavaProgram(ctx context.Context, options BuildJavaProgramOptions) (*JavaProgram, error) {
+ if options.BaseDir == "" {
+ return nil, fmt.Errorf("base dir required")
+ } else if _, err := os.Stat(filepath.Join(options.BaseDir, "build.gradle")); err != nil {
+ return nil, fmt.Errorf("failed finding build.gradle in base dir: %w", err)
+ } else if options.HarnessDependency == "" {
+ return nil, fmt.Errorf("harness dependency required")
+ } else if options.MainClass == "" {
+ return nil, fmt.Errorf("main class required")
+ }
+
+ // Create temp dir if needed that we will remove if creating is unsuccessful
+ success := false
+ var dir string
+ if options.DirName != "" {
+ dir = filepath.Join(options.BaseDir, options.DirName)
+ } else {
+ var err error
+ dir, err = os.MkdirTemp(options.BaseDir, "program-")
+ if err != nil {
+ return nil, fmt.Errorf("failed making temp dir: %w", err)
+ }
+ defer func() {
+ if !success {
+ // Intentionally swallow error
+ _ = os.RemoveAll(dir)
+ }
+ }()
+ }
+ j := &JavaProgram{dir}
+
+ // If we depend on SDK via path, built it and get the JAR
+ isPathDep := strings.ContainsAny(options.Version, `/\`)
+ if isPathDep {
+ cmd := j.buildGradleCommand(ctx, options.Version, false, options.ApplyToCommand, "jar", "gatherRuntimeDeps")
+ if err := cmd.Run(); err != nil {
+ return nil, fmt.Errorf("failed building Java SDK: %w", err)
+ }
+ // Copy JARs to sdkjars dir
+ sdkJarsDir := filepath.Join(dir, "sdkjars")
+ if err := copy.Copy(filepath.Join(options.Version, "temporal-sdk/build/libs"), sdkJarsDir); err != nil {
+ return nil, fmt.Errorf("failed copying lib JARs: %w", err)
+ }
+ if err := copy.Copy(filepath.Join(options.Version, "temporal-sdk/build/runtimeDeps"), sdkJarsDir); err != nil {
+ return nil, fmt.Errorf("failed copying runtime JARs: %w", err)
+ }
+ }
+
+ // Create build.gradle and settings.gradle
+ temporalSDKDependency := ""
+ if isPathDep {
+ temporalSDKDependency = "implementation fileTree(dir: 'sdkjars', include: ['*.jar'])"
+ } else if options.Version != "" {
+ temporalSDKDependency = fmt.Sprintf("implementation 'io.temporal:temporal-sdk:%v'",
+ strings.TrimPrefix(options.Version, "v"))
+ }
+ buildGradle := `
+plugins {
+ id 'application'
+}
+
+repositories {
+ maven {
+ url "https://oss.sonatype.org/content/repositories/snapshots/"
+ }
+ mavenCentral()
+}
+
+dependencies {
+ implementation '` + options.HarnessDependency + `'
+ ` + temporalSDKDependency + `
+}
+
+application {
+ mainClass = '` + options.MainClass + `'
+}`
+ if err := os.WriteFile(filepath.Join(dir, "build.gradle"), []byte(buildGradle), 0644); err != nil {
+ return nil, fmt.Errorf("failed writing build.gradle: %w", err)
+ }
+ settingsGradle := fmt.Sprintf("rootProject.name = '%v'", filepath.Base(dir))
+ if err := os.WriteFile(filepath.Join(dir, "settings.gradle"), []byte(settingsGradle), 0644); err != nil {
+ return nil, fmt.Errorf("failed writing settings.gradle: %w", err)
+ }
+
+ // Build if wanted
+ if options.Build {
+ // This is really only to prime the system-level caches. The build won't be
+ // used by run.
+ cmd := j.buildGradleCommand(ctx, dir, true,
+ options.ApplyToCommand, "--no-daemon", "--include-build", "../", "build")
+ if err := cmd.Run(); err != nil {
+ return nil, err
+ }
+ }
+
+ success = true
+ return j, nil
+}
+
+// JavaProgramFromDir recreates the Java program from a Dir() result of a
+// BuildJavaProgram(). Note, the base directory of dir when it was built must
+// also be present.
+func JavaProgramFromDir(dir string) (*JavaProgram, error) {
+ // Quick sanity check on the presence of build.gradle here _and_ in base
+ if _, err := os.Stat(filepath.Join(dir, "build.gradle")); err != nil {
+ return nil, fmt.Errorf("failed finding build.gradle in dir: %w", err)
+ } else if _, err := os.Stat(filepath.Join(dir, "../build.gradle")); err != nil {
+ return nil, fmt.Errorf("failed finding build.gradle in base dir: %w", err)
+ }
+ return &JavaProgram{dir}, nil
+}
+
+// Dir is the directory to run in.
+func (j *JavaProgram) Dir() string { return j.dir }
+
+// NewCommand makes a new command for the given args.
+func (j *JavaProgram) NewCommand(ctx context.Context, args ...string) (*exec.Cmd, error) {
+ // Since args have to be a string, we disallow quotes
+ var argsStr string
+ for _, arg := range args {
+ if strings.ContainsAny(arg, `"'`) {
+ return nil, fmt.Errorf("java argument cannot contain single or double quote")
+ }
+ if argsStr != "" {
+ argsStr += " "
+ }
+ argsStr += "'" + arg + "'"
+ }
+ return j.buildGradleCommand(ctx, j.dir, true, nil, "--include-build", "../", "run", "--args", argsStr), nil
+}
+
+func (j *JavaProgram) buildGradleCommand(
+ ctx context.Context,
+ dir string,
+ gradleInParentDir bool,
+ applyToCommand func(context.Context, *exec.Cmd) error,
+ args ...string,
+) *exec.Cmd {
+ // Prepare exe whether windows or not
+ var exe string
+ if runtime.GOOS == "windows" {
+ exe = "cmd.exe"
+ if gradleInParentDir {
+ args = append([]string{"/C", "..\\gradlew"}, args...)
+ } else {
+ args = append([]string{"/C", "gradlew"}, args...)
+ }
+ } else {
+ exe = "/bin/sh"
+ if gradleInParentDir {
+ args = append([]string{"../gradlew"}, args...)
+ } else {
+ args = append([]string{"gradlew"}, args...)
+ }
+ }
+
+ cmd := exec.CommandContext(ctx, exe, args...)
+ cmd.Dir = dir
+ cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
+ return cmd
+}
diff --git a/sdkbuild/python.go b/sdkbuild/python.go
new file mode 100644
index 0000000..37ca72e
--- /dev/null
+++ b/sdkbuild/python.go
@@ -0,0 +1,154 @@
+package sdkbuild
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+ "strconv"
+ "strings"
+)
+
+// BuildPythonProgramOptions are options for BuildPythonProgram.
+type BuildPythonProgramOptions struct {
+ // Directory that will have a temporary directory created underneath. This
+ // should be a Poetry project with a pyproject.toml.
+ BaseDir string
+ // Required version. If it contains a slash it is assumed to be a path with
+ // a single wheel in the dist directory. Otherwise it is a specific version
+ // (with leading "v" is trimmed if present).
+ Version string
+ // Required Poetry dependency name of BaseDir.
+ DependencyName string
+ // If present, this directory is expected to exist beneath base dir. Otherwise
+ // a temporary dir is created.
+ DirName string
+ // If present, applied to build commands before run. May be called multiple
+ // times for a single build.
+ ApplyToCommand func(context.Context, *exec.Cmd) error
+}
+
+// PythonProgram is a Python-specific implementation of Program.
+type PythonProgram struct {
+ dir string
+}
+
+var _ Program = (*PythonProgram)(nil)
+
+// BuildPythonProgram builds a Python program. If completed successfully, this
+// can be stored and re-obtained via PythonProgramFromDir() with the Dir() value
+// (but the entire BaseDir must be present too).
+func BuildPythonProgram(ctx context.Context, options BuildPythonProgramOptions) (*PythonProgram, error) {
+ if options.BaseDir == "" {
+ return nil, fmt.Errorf("base dir required")
+ } else if options.Version == "" {
+ return nil, fmt.Errorf("version required")
+ } else if _, err := os.Stat(filepath.Join(options.BaseDir, "pyproject.toml")); err != nil {
+ return nil, fmt.Errorf("failed finding pyproject.toml in base dir: %w", err)
+ } else if options.DependencyName == "" {
+ return nil, fmt.Errorf("dependency name required")
+ }
+
+ // Create temp dir if needed that we will remove if creating is unsuccessful
+ success := false
+ var dir string
+ if options.DirName != "" {
+ dir = filepath.Join(options.BaseDir, options.DirName)
+ } else {
+ var err error
+ dir, err = os.MkdirTemp(options.BaseDir, "program-")
+ if err != nil {
+ return nil, fmt.Errorf("failed making temp dir: %w", err)
+ }
+ defer func() {
+ if !success {
+ // Intentionally swallow error
+ _ = os.RemoveAll(dir)
+ }
+ }()
+ }
+
+ // Use semantic version or path if it's a path
+ versionStr := strconv.Quote(strings.TrimPrefix(options.Version, "v"))
+ if strings.ContainsAny(options.Version, `/\`) {
+ // We expect a dist/ directory with a single whl file present
+ wheels, err := filepath.Glob(filepath.Join(options.Version, "dist/*.whl"))
+ if err != nil {
+ return nil, fmt.Errorf("failed glob wheel lookup: %w", err)
+ } else if len(wheels) != 1 {
+ return nil, fmt.Errorf("expected single dist wheel, found %v", wheels)
+ }
+ absWheel, err := filepath.Abs(wheels[0])
+ if err != nil {
+ return nil, fmt.Errorf("unable to make wheel path absolute: %w", err)
+ }
+ // There's a strange bug in Poetry or somewhere deeper where, on Windows,
+ // the single drive letter has to be capitalized
+ if runtime.GOOS == "windows" && absWheel[1] == ':' {
+ absWheel = strings.ToUpper(absWheel[:1]) + absWheel[1:]
+ }
+ versionStr = "{ path = " + strconv.Quote(absWheel) + " }"
+ }
+ pyProjectTOML := `
+[tool.poetry]
+name = "python-program-` + filepath.Base(dir) + `"
+version = "0.1.0"
+description = "Temporal SDK Python Test"
+authors = ["Temporal Technologies Inc "]
+
+[tool.poetry.dependencies]
+python = "^3.8"
+temporalio = ` + versionStr + `
+` + options.DependencyName + ` = { path = "../" }
+
+[build-system]
+requires = ["poetry-core>=1.0.0"]
+build-backend = "poetry.core.masonry.api"`
+ if err := os.WriteFile(filepath.Join(dir, "pyproject.toml"), []byte(pyProjectTOML), 0644); err != nil {
+ return nil, fmt.Errorf("failed writing pyproject.toml: %w", err)
+ }
+
+ // Install
+ cmd := exec.CommandContext(ctx, "poetry", "install", "--no-dev", "--no-root", "-v")
+ cmd.Dir = dir
+ cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
+ if options.ApplyToCommand != nil {
+ if err := options.ApplyToCommand(ctx, cmd); err != nil {
+ return nil, err
+ }
+ }
+ if err := cmd.Run(); err != nil {
+ return nil, fmt.Errorf("failed installing: %w", err)
+ }
+
+ success = true
+ return &PythonProgram{dir}, nil
+}
+
+// PythonProgramFromDir recreates the Python program from a Dir() result of a
+// BuildPythonProgram(). Note, the base directory of dir when it was built must
+// also be present.
+func PythonProgramFromDir(dir string) (*PythonProgram, error) {
+ // Quick sanity check on the presence of pyproject.toml here _and_ in base
+ if _, err := os.Stat(filepath.Join(dir, "pyproject.toml")); err != nil {
+ return nil, fmt.Errorf("failed finding pyproject.toml in dir: %w", err)
+ } else if _, err := os.Stat(filepath.Join(dir, "../pyproject.toml")); err != nil {
+ return nil, fmt.Errorf("failed finding pyproject.toml in base dir: %w", err)
+ }
+ return &PythonProgram{dir}, nil
+}
+
+// Dir is the directory to run in.
+func (p *PythonProgram) Dir() string { return p.dir }
+
+// NewCommand makes a new Poetry command. The first argument needs to be the
+// name of the module.
+func (p *PythonProgram) NewCommand(ctx context.Context, args ...string) (*exec.Cmd, error) {
+ args = append([]string{"run", "python", "-m"}, args...)
+ cmd := exec.CommandContext(ctx, "poetry", args...)
+ cmd.Dir = p.dir
+ cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
+ return cmd, nil
+}
diff --git a/sdkbuild/sdkbuild.go b/sdkbuild/sdkbuild.go
new file mode 100644
index 0000000..e03efbd
--- /dev/null
+++ b/sdkbuild/sdkbuild.go
@@ -0,0 +1,19 @@
+// Package sdkbuild provides helpers to build and run projects with SDKs across
+// languages and versions.
+package sdkbuild
+
+import (
+ "context"
+ "os/exec"
+)
+
+// Program is a built SDK program that can be run.
+type Program interface {
+ // Dir is the directory the program is in. If created on the fly, usually this
+ // temporary directory is deleted after use.
+ Dir() string
+
+ // NewCommand creates a new command for the program with given args and with
+ // stdio set as the current stdio.
+ NewCommand(ctx context.Context, args ...string) (*exec.Cmd, error)
+}
diff --git a/sdkbuild/typescript.go b/sdkbuild/typescript.go
new file mode 100644
index 0000000..a49b116
--- /dev/null
+++ b/sdkbuild/typescript.go
@@ -0,0 +1,263 @@
+package sdkbuild
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strconv"
+ "strings"
+)
+
+// BuildTypeScriptProgramOptions are options for BuildTypeScriptProgram.
+type BuildTypeScriptProgramOptions struct {
+ // Directory that will have a temporary directory created underneath.
+ BaseDir string
+ // Required version. If it contains a slash it is assumed to be a path with a
+ // package.json. Otherwise it is a specific version (with leading "v" is
+ // trimmed if present).
+ Version string
+ // Required set of paths to include in tsconfig.json paths for the project.
+ // The paths should be relative to one-directory beneath BaseDir.
+ TSConfigPaths map[string][]string
+ // If present, this directory is expected to exist beneath base dir. Otherwise
+ // a temporary dir is created.
+ DirName string
+ // If present, applied to build commands before run. May be called multiple
+ // times for a single build.
+ ApplyToCommand func(context.Context, *exec.Cmd) error
+ // If present, overrides the default "include" array in tsconfig.json.
+ Includes []string
+ // If present, overrides the default "exclude" array in tsconfig.json.
+ Excludes []string
+ // If present, add additional dependencies -> version string to package.json.
+ MoreDependencies map[string]string
+}
+
+// TypeScriptProgram is a TypeScript-specific implementation of Program.
+type TypeScriptProgram struct {
+ dir string
+}
+
+var _ Program = (*TypeScriptProgram)(nil)
+
+// BuildTypeScriptProgram builds a TypeScript program. If completed
+// successfully, this can be stored and re-obtained via
+// TypeScriptProgramFromDir() with the Dir() value (but the entire BaseDir must
+// be present too).
+func BuildTypeScriptProgram(ctx context.Context, options BuildTypeScriptProgramOptions) (*TypeScriptProgram, error) {
+ if options.BaseDir == "" {
+ return nil, fmt.Errorf("base dir required")
+ } else if options.Version == "" {
+ return nil, fmt.Errorf("version required")
+ } else if len(options.TSConfigPaths) == 0 {
+ return nil, fmt.Errorf("at least one tsconfig path required")
+ }
+
+ // Create temp dir if needed that we will remove if creating is unsuccessful
+ success := false
+ var dir string
+ if options.DirName != "" {
+ dir = filepath.Join(options.BaseDir, options.DirName)
+ } else {
+ var err error
+ dir, err = os.MkdirTemp(options.BaseDir, "program-")
+ if err != nil {
+ return nil, fmt.Errorf("failed making temp dir: %w", err)
+ }
+ defer func() {
+ if !success {
+ // Intentionally swallow error
+ _ = os.RemoveAll(dir)
+ }
+ }()
+ }
+
+ // Create package JSON
+ var packageJSONDepStr string
+ if strings.ContainsAny(options.Version, `/\`) {
+ if _, err := os.Stat(filepath.Join(options.Version, "package.json")); err != nil {
+ return nil, fmt.Errorf("failed finding package.json in version dir: %w", err)
+ }
+
+ // Have to build the local repo
+ if st, err := os.Stat(filepath.Join(options.Version, "node_modules")); err != nil || !st.IsDir() {
+ // Only install dependencies, avoid triggerring any post install build scripts
+ cmd := exec.CommandContext(ctx, "npm", "ci", "--ignore-scripts")
+ cmd.Dir = options.Version
+ cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
+ if err := cmd.Run(); err != nil {
+ return nil, fmt.Errorf("failed installing SDK deps: %w", err)
+ }
+
+ // Build the SDK, ignore the unused `create` package as a mostly insignificant micro optimisation.
+ cmd = exec.CommandContext(ctx, "npm", "run", "build", "--", "--ignore", "@temporalio/create")
+ cmd.Dir = options.Version
+ cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
+ if err := cmd.Run(); err != nil {
+ return nil, fmt.Errorf("failed building SDK: %w", err)
+ }
+ }
+
+ // Create package.json updates
+ localPath, err := filepath.Abs(options.Version)
+ if err != nil {
+ return nil, fmt.Errorf("cannot get absolute path from version path: %w", err)
+ }
+ pkgs := []string{"activity", "client", "common", "internal-workflow-common",
+ "internal-non-workflow-common", "proto", "worker", "workflow"}
+ for _, pkg := range pkgs {
+ pkgPath := "file:" + filepath.Join(localPath, "packages", pkg)
+ packageJSONDepStr += fmt.Sprintf(`"@temporalio/%v": %q,`, pkg, pkgPath)
+ packageJSONDepStr += "\n "
+ }
+ } else {
+ version := strings.TrimPrefix(options.Version, "v")
+ pkgs := []string{"activity", "client", "common", "worker", "workflow"}
+ for _, pkg := range pkgs {
+ packageJSONDepStr += fmt.Sprintf(` "@temporalio/%v": %q,`, pkg, version) + "\n"
+ }
+ }
+ moreDeps := ""
+ for dep, version := range options.MoreDependencies {
+ moreDeps += fmt.Sprintf(` "%v": "%v",`, dep, version) + "\n"
+ }
+
+ packageJSON := `{
+ "name": "program",
+ "private": true,
+ "scripts": {
+ "build": "tsc --build"
+ },
+ "dependencies": {
+ ` + packageJSONDepStr + `
+ ` + moreDeps + `
+ "commander": "^8.3.0",
+ "ms": "^3.0.0-canary.1",
+ "proto3-json-serializer": "^1.1.1",
+ "uuid": "^8.3.2"
+ },
+ "devDependencies": {
+ "@tsconfig/node16": "^1.0.0",
+ "@types/node": "^16.11.59",
+ "@types/uuid": "^8.3.4",
+ "tsconfig-paths": "^3.12.0",
+ "typescript": "^4.4.2"
+ }
+}`
+ if err := os.WriteFile(filepath.Join(dir, "package.json"), []byte(packageJSON), 0644); err != nil {
+ return nil, fmt.Errorf("failed writing package.json: %w", err)
+ }
+
+ // Create tsconfig
+ var tsConfigPathStr string
+ for name, paths := range options.TSConfigPaths {
+ if len(paths) == 0 {
+ return nil, fmt.Errorf("harness path slice is empty")
+ }
+ tsConfigPathStr += fmt.Sprintf("%q: [", name)
+ for i, path := range paths {
+ if i > 0 {
+ tsConfigPathStr += ", "
+ }
+ tsConfigPathStr += strconv.Quote(path)
+ }
+ tsConfigPathStr += "],\n "
+ }
+ includes := []string{"../features/**/*.ts", "../harness/ts/**/*.ts"}
+ if len(options.Includes) > 0 {
+ includes = options.Includes
+ }
+ excludes := []string{"../node_modules", "../harness/go", "../harness/java"}
+ if len(options.Excludes) > 0 {
+ excludes = options.Excludes
+ }
+ quotedIncludes := make([]string, len(includes))
+ for i, include := range includes {
+ quotedIncludes[i] = strconv.Quote(include)
+ }
+ quotedExcludes := make([]string, len(excludes))
+ for i, exclude := range excludes {
+ quotedExcludes[i] = strconv.Quote(exclude)
+ }
+ tsConfig := `{
+ "extends": "@tsconfig/node16/tsconfig.json",
+ "version": "4.4.2",
+ "compilerOptions": {
+ "baseUrl": ".",
+ "outDir": "./tslib",
+ "rootDirs": ["../", "."],
+ "paths": {
+ ` + tsConfigPathStr + `
+ "*": ["node_modules/*", "node_modules/@types/*"]
+ },
+ "typeRoots": ["node_modules/@types"],
+ "module": "commonjs",
+ "moduleResolution": "node",
+ "sourceMap": true,
+ "resolveJsonModule": true,
+ "declaration": true,
+ "declarationMap": true,
+ "allowJs": true
+ },
+ "include": [` + strings.Join(quotedIncludes, ", ") + `],
+ "exclude": [` + strings.Join(quotedExcludes, ", ") + `]
+}`
+ if err := os.WriteFile(filepath.Join(dir, "tsconfig.json"), []byte(tsConfig), 0644); err != nil {
+ return nil, fmt.Errorf("failed writing tsconfig.json: %w", err)
+ }
+
+ // Install
+ cmd := exec.CommandContext(ctx, "npm", "install")
+ cmd.Dir = dir
+ cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
+ if options.ApplyToCommand != nil {
+ if err := options.ApplyToCommand(ctx, cmd); err != nil {
+ return nil, err
+ }
+ }
+ if err := cmd.Run(); err != nil {
+ return nil, fmt.Errorf("failed installing: %w", err)
+ }
+
+ // Compile
+ cmd = exec.CommandContext(ctx, "npm", "run", "build")
+ cmd.Dir = dir
+ cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
+ if options.ApplyToCommand != nil {
+ if err := options.ApplyToCommand(ctx, cmd); err != nil {
+ return nil, err
+ }
+ }
+ if err := cmd.Run(); err != nil {
+ return nil, fmt.Errorf("failed compiling: %w", err)
+ }
+
+ success = true
+ return &TypeScriptProgram{dir}, nil
+}
+
+// TypeScriptProgramFromDir recreates the TypeScript program from a Dir() result
+// of a BuildTypeScriptProgram(). Note, the base directory of dir when it was
+// built must also be present.
+func TypeScriptProgramFromDir(dir string) (*TypeScriptProgram, error) {
+ // Quick sanity check on the presence of package.json here
+ if _, err := os.Stat(filepath.Join(dir, "package.json")); err != nil {
+ return nil, fmt.Errorf("failed finding package.json in dir: %w", err)
+ }
+ return &TypeScriptProgram{dir}, nil
+}
+
+// Dir is the directory to run in.
+func (t *TypeScriptProgram) Dir() string { return t.dir }
+
+// NewCommand makes a new Node command. The first argument needs to be the name
+// of the script.
+func (t *TypeScriptProgram) NewCommand(ctx context.Context, args ...string) (*exec.Cmd, error) {
+ args = append([]string{"-r", "tsconfig-paths/register"}, args...)
+ cmd := exec.CommandContext(ctx, "node", args...)
+ cmd.Dir = t.dir
+ cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
+ return cmd, nil
+}