September 2, 2018

For the past couple of months I have been working on a script library tool written in Go. This is a tool that I wanted to sell and I wanted a way for people who were interested to give the tool a test drive. A lot of tools today need to connect to the internet in order to function which makes it easy to control access since the server can track when a client application was first used and can simply forbid access to that application instance after the trial period has passed. The tool I was building is intended to be run completely locally. I needed a way to build a binary that will have full functionality until a certain amount of time has passed at which point it would only print a message directing the user to purchase the full version of the tool. Not really malicious code but essentially disabling the application after a certain condition is met (the trial period ended), a logic bomb.

The Go compiler has the ability to set string variables when building binaries. First we need to create a go source file that looks like this:

package main import ( "fmt" ) // note that the variable can be public or private var buildVar string func main() { // for now let's just print the variable fmt.Println("buildVar:", buildVar) }

If we build and run this with no arguments we see that buildVar is not initialized.

$ go build -o myapp main.go $ ./myapp buildVar:

In order to set the variable at build time we need the -ldflags argument for `go build/install

$ go build -o myapp -ldflags "-X 'main.buildVar="injected"'" main.go $ ./myapp buildVar: injected

There are a couple of gotchas with this feature. First, you can only set string variables. Second, if you are setting values in any package other than main then you need to specify the full package name (ex. github.com/my/super-app/lib.SuperAppVar="injected" )

So now that we know how to dynamically inject values into our application at build time let’s use this knowledge to build a trial version of our application. We will inject the date of the build and a number as a string to indicate the number of time units we want the trial to last. Our simple application file now looks like this:

package main import ( "fmt" "os" "strconv" "time" ) var ( trialStart, trialLength string ) func init() { if len(trialStart) == 0 || len(trialLength) == 0 { // no-op if no values are set return } trialStartDate, startErr := time.Parse(time.UnixDate, trialStart) trialLengthVal, lenErr := strconv.Atoi(trialLength) if startErr != nil || lenErr != nil { // no-op if we have any issues parsing the values return } // we'll use minutes here just so we don't have to wait around // forever to see the results trialEnds := trialStartDate.Add(time.Duration(trialLengthVal) * time.Minute) if time.Now().After(trialEnds) { fmt.Println("Your trial has ended.") os.Exit(0) } else { fmt.Println("Your trial expires on", trialEnds) } } func main() { fmt.Println("Hello World from My App.") }

So we can now build it and specify a Unix formatted date (with the date command) and the number of minutes we want the trial to last (obviously we would use hours or something larger for an actual trial).

$ go build -o myapp -ldflags \ "-X 'main.trialStart=$(date)' -X main.trialLength="5"'" \ main.go $ ./myapp Your trial expires on 2018-09-02 23:24:46 -0400 EDT Hello World from My App. $ ./myapp # 5+ minutes later Your trial has ended.

Hopefully this has shown you how to dynamically inject variable values into your applications at compile time. You can use this feature for sharing a trial version of your application as I showed above, embed a version string into the application or anything else you would like (so long as it’s a string).

27 Kudos