Support product variables

Allow modules to vary their properties based on product variables.
For now, DEVICE_USES_LOGD, DEVICE_USES_JEMALLOC, and DEVICE_USES_DLMALLOC,
and BOARD_MALLOC_ALIGNMENT are supported.

Product variables can provide a value (only bool and int supported for
now), and if any of the product variable properties contains a "%d"
then Sprintf will be called with the property value as the format
and the product variable value convert to an int as the only argument.

For example:

    product_variables: {
        dlmalloc_alignment: {
            cflags: ["-DMALLOC_ALIGNMENT=%d"],
        },
    },

will cause -DMALLOC_ALIGNMENT=16 to be added to any top level
properties called "cflags".

Change-Id: I74882a6ab4914d3e222f8d06cfac371b7b829ae5
diff --git a/Android.bp b/Android.bp
index f001246..404c478 100644
--- a/Android.bp
+++ b/Android.bp
@@ -97,10 +97,12 @@
         "common/config.go",
         "common/defs.go",
         "common/env.go",
+        "common/extend.go",
         "common/glob.go",
         "common/module.go",
         "common/paths.go",
         "common/util.go",
+        "common/variable.go",
     ],
 }
 
diff --git a/androidbp/cmd/androidbp.go b/androidbp/cmd/androidbp.go
index b1e364c..3edd243 100644
--- a/androidbp/cmd/androidbp.go
+++ b/androidbp/cmd/androidbp.go
@@ -175,6 +175,44 @@
 	return
 }
 
+func translateProductVariableConditionals(props []*bpparser.Property) (computedProps []string, err error) {
+	for _, productVariable := range props {
+		v, ok := productVariableConditionals[productVariable.Name.Name]
+		if !ok {
+			return nil, fmt.Errorf("Unsupported product variable %q", productVariable.Name.Name)
+		}
+
+		var scopedProps []string
+		for _, conditionalScopedProp := range productVariable.Value.MapValue {
+			if assignment, ok, err := translateSingleProperty(conditionalScopedProp); err != nil {
+				return nil, err
+			} else if ok {
+				assignment.assigner = "+="
+				a := assignment.assignment()
+				if v.value != "" && strings.Contains(a, "%d") {
+					a = strings.Replace(a, "%d", v.value, 1)
+				}
+				scopedProps = append(scopedProps, a)
+			} else {
+				return nil, fmt.Errorf("Unsupported product variable property %q",
+					conditionalScopedProp.Name.Name)
+			}
+		}
+
+		if len(scopedProps) > 0 {
+			if v.conditional != "" {
+				computedProps = append(computedProps, v.conditional)
+				computedProps = append(computedProps, scopedProps...)
+				computedProps = append(computedProps, "endif")
+			} else {
+				computedProps = append(computedProps, scopedProps...)
+			}
+		}
+	}
+
+	return computedProps, nil
+}
+
 var secondTargetReplacer = strings.NewReplacer("TARGET_", "TARGET_2ND_")
 
 func translateSuffixProperties(suffixProps []*bpparser.Property,
@@ -303,6 +341,12 @@
 				return err
 			}
 			standardProps = append(standardProps, props...)
+		} else if "product_variables" == prop.Name.Name {
+			props, err := translateProductVariableConditionals(prop.Value.MapValue)
+			if err != nil {
+				return err
+			}
+			standardProps = append(standardProps, props...)
 		} else if _, ok := ignoredProperties[prop.Name.Name]; ok {
 		} else {
 			return fmt.Errorf("Unsupported property %q", prop.Name.Name)
diff --git a/androidbp/cmd/soong.go b/androidbp/cmd/soong.go
index 1dffe4a..e9d421a 100644
--- a/androidbp/cmd/soong.go
+++ b/androidbp/cmd/soong.go
@@ -172,3 +172,10 @@
 	"BUILD_NATIVE_TEST":    "BUILD_HOST_NATIVE_TEST",
 	"BUILD_JAVA_LIBRARY":   "BUILD_HOST_JAVA_LIBRARY",
 }
+
+var productVariableConditionals = map[string]struct{conditional, value string}{
+	"device_uses_jemalloc": {"ifneq ($(MALLOC_IMPL),dlmalloc)", ""},
+	"device_uses_dlmalloc": {"ifeq ($(MALLOC_IMPL),dlmalloc)", ""},
+	"device_uses_logd":     {"ifneq ($(TARGET_USES_LOGD),false)", ""},
+	"dlmalloc_alignment":   {"ifdef DLMALLOC_ALIGNMENT", "$(DLMALLOC_ALIGNMENT)"},
+}
diff --git a/common/arch.go b/common/arch.go
index 3d685e5..b9e3a5c 100644
--- a/common/arch.go
+++ b/common/arch.go
@@ -450,8 +450,12 @@
 		return
 	}
 
+	callback := func(srcPropertyName, dstPropertyName string) {
+		a.extendedProperties[dstPropertyName] = struct{}{}
+	}
+
 	for i := range a.generalProperties {
-		generalPropsValue := reflect.ValueOf(a.generalProperties[i]).Elem()
+		generalPropsValue := []reflect.Value{reflect.ValueOf(a.generalProperties[i]).Elem()}
 
 		// Handle arch-specific properties in the form:
 		// arch: {
@@ -461,8 +465,8 @@
 		// },
 		t := arch.ArchType
 		field := proptools.FieldNameForProperty(t.Name)
-		a.extendProperties(ctx, "arch", t.Name, generalPropsValue,
-			reflect.ValueOf(a.archProperties[i].Arch).FieldByName(field).Elem().Elem())
+		extendProperties(ctx, "arch_variant", "arch."+t.Name, generalPropsValue,
+			reflect.ValueOf(a.archProperties[i].Arch).FieldByName(field).Elem().Elem(), callback)
 
 		// Handle arch-variant-specific properties in the form:
 		// arch: {
@@ -473,8 +477,8 @@
 		v := dashToUnderscoreReplacer.Replace(arch.ArchVariant)
 		if v != "" {
 			field := proptools.FieldNameForProperty(v)
-			a.extendProperties(ctx, "arch", v, generalPropsValue,
-				reflect.ValueOf(a.archProperties[i].Arch).FieldByName(field).Elem().Elem())
+			extendProperties(ctx, "arch_variant", "arch."+v, generalPropsValue,
+				reflect.ValueOf(a.archProperties[i].Arch).FieldByName(field).Elem().Elem(), callback)
 		}
 
 		// Handle cpu-variant-specific properties in the form:
@@ -486,8 +490,8 @@
 		c := dashToUnderscoreReplacer.Replace(arch.CpuVariant)
 		if c != "" {
 			field := proptools.FieldNameForProperty(c)
-			a.extendProperties(ctx, "arch", c, generalPropsValue,
-				reflect.ValueOf(a.archProperties[i].Arch).FieldByName(field).Elem().Elem())
+			extendProperties(ctx, "arch_variant", "arch."+c, generalPropsValue,
+				reflect.ValueOf(a.archProperties[i].Arch).FieldByName(field).Elem().Elem(), callback)
 		}
 
 		// Handle multilib-specific properties in the form:
@@ -497,8 +501,8 @@
 		//     },
 		// },
 		multilibField := proptools.FieldNameForProperty(t.Multilib)
-		a.extendProperties(ctx, "multilib", t.Multilib, generalPropsValue,
-			reflect.ValueOf(a.archProperties[i].Multilib).FieldByName(multilibField).Elem().Elem())
+		extendProperties(ctx, "arch_variant", "multilib."+t.Multilib, generalPropsValue,
+			reflect.ValueOf(a.archProperties[i].Multilib).FieldByName(multilibField).Elem().Elem(), callback)
 
 		// Handle host-or-device-specific properties in the form:
 		// target: {
@@ -508,8 +512,8 @@
 		// },
 		hodProperty := hod.Property()
 		hodField := proptools.FieldNameForProperty(hodProperty)
-		a.extendProperties(ctx, "target", hodProperty, generalPropsValue,
-			reflect.ValueOf(a.archProperties[i].Target).FieldByName(hodField).Elem().Elem())
+		extendProperties(ctx, "arch_variant", "target."+hodProperty, generalPropsValue,
+			reflect.ValueOf(a.archProperties[i].Target).FieldByName(hodField).Elem().Elem(), callback)
 
 		// Handle host target properties in the form:
 		// target: {
@@ -538,15 +542,15 @@
 		if hod.Host() {
 			for _, v := range osList {
 				if v.goos == runtime.GOOS {
-					a.extendProperties(ctx, "target", v.goos, generalPropsValue,
-						reflect.ValueOf(a.archProperties[i].Target).FieldByName(v.field).Elem().Elem())
+					extendProperties(ctx, "arch_variant", "target."+v.goos, generalPropsValue,
+						reflect.ValueOf(a.archProperties[i].Target).FieldByName(v.field).Elem().Elem(), callback)
 					t := arch.ArchType
-					a.extendProperties(ctx, "target", v.goos+"_"+t.Name, generalPropsValue,
-						reflect.ValueOf(a.archProperties[i].Target).FieldByName(v.field+"_"+t.Name).Elem().Elem())
+					extendProperties(ctx, "arch_variant", "target."+v.goos+"_"+t.Name, generalPropsValue,
+						reflect.ValueOf(a.archProperties[i].Target).FieldByName(v.field+"_"+t.Name).Elem().Elem(), callback)
 				}
 			}
-			a.extendProperties(ctx, "target", "not_windows", generalPropsValue,
-				reflect.ValueOf(a.archProperties[i].Target).FieldByName("Not_windows").Elem().Elem())
+			extendProperties(ctx, "arch_variant", "target.not_windows", generalPropsValue,
+				reflect.ValueOf(a.archProperties[i].Target).FieldByName("Not_windows").Elem().Elem(), callback)
 		}
 
 		// Handle 64-bit device properties in the form:
@@ -564,11 +568,11 @@
 		// debuggerd that need to know when they are a 32-bit process running on a 64-bit device
 		if hod.Device() {
 			if true /* && target_is_64_bit */ {
-				a.extendProperties(ctx, "target", "android64", generalPropsValue,
-					reflect.ValueOf(a.archProperties[i].Target).FieldByName("Android64").Elem().Elem())
+				extendProperties(ctx, "arch_variant", "target.android64", generalPropsValue,
+					reflect.ValueOf(a.archProperties[i].Target).FieldByName("Android64").Elem().Elem(), callback)
 			} else {
-				a.extendProperties(ctx, "target", "android32", generalPropsValue,
-					reflect.ValueOf(a.archProperties[i].Target).FieldByName("Android32").Elem().Elem())
+				extendProperties(ctx, "arch_variant", "target.android32", generalPropsValue,
+					reflect.ValueOf(a.archProperties[i].Target).FieldByName("Android32").Elem().Elem(), callback)
 			}
 		}
 
@@ -583,8 +587,8 @@
 		// },
 		if hod.Device() {
 			t := arch.ArchType
-			a.extendProperties(ctx, "target", "android_"+t.Name, generalPropsValue,
-				reflect.ValueOf(a.archProperties[i].Target).FieldByName("Android_"+t.Name).Elem().Elem())
+			extendProperties(ctx, "arch_variant", "target.android_"+t.Name, generalPropsValue,
+				reflect.ValueOf(a.archProperties[i].Target).FieldByName("Android_"+t.Name).Elem().Elem(), callback)
 		}
 
 		if ctx.Failed() {
@@ -607,93 +611,3 @@
 		panic(fmt.Errorf("Unsupported kind %s", v.Kind()))
 	}
 }
-
-// TODO: move this to proptools
-func (a *AndroidModuleBase) extendProperties(ctx blueprint.EarlyMutatorContext, variationType, variationName string,
-	dstValue, srcValue reflect.Value) {
-	a.extendPropertiesRecursive(ctx, variationType, variationName, dstValue, srcValue, "")
-}
-
-func (a *AndroidModuleBase) extendPropertiesRecursive(ctx blueprint.EarlyMutatorContext, variationType, variationName string,
-	dstValue, srcValue reflect.Value, recursePrefix string) {
-
-	typ := dstValue.Type()
-	if srcValue.Type() != typ {
-		panic(fmt.Errorf("can't extend mismatching types (%s <- %s)",
-			dstValue.Kind(), srcValue.Kind()))
-	}
-
-	for i := 0; i < srcValue.NumField(); i++ {
-		field := typ.Field(i)
-		if field.PkgPath != "" {
-			// The field is not exported so just skip it.
-			continue
-		}
-
-		srcFieldValue := srcValue.Field(i)
-		dstFieldValue := dstValue.Field(i)
-
-		localPropertyName := proptools.PropertyNameForField(field.Name)
-		propertyName := fmt.Sprintf("%s.%s.%s%s", variationType, variationName,
-			recursePrefix, localPropertyName)
-		propertyPresentInVariation := ctx.ContainsProperty(propertyName)
-
-		if !propertyPresentInVariation {
-			continue
-		}
-
-		tag := field.Tag.Get("android")
-		tags := map[string]bool{}
-		for _, entry := range strings.Split(tag, ",") {
-			if entry != "" {
-				tags[entry] = true
-			}
-		}
-
-		if !tags["arch_variant"] {
-			ctx.PropertyErrorf(propertyName, "property %q can't be specific to a build variant",
-				recursePrefix+proptools.PropertyNameForField(field.Name))
-			continue
-		}
-
-		if !ctx.ContainsProperty(propertyName) {
-			continue
-		}
-		a.extendedProperties[localPropertyName] = struct{}{}
-
-		switch srcFieldValue.Kind() {
-		case reflect.Bool:
-			// Replace the original value.
-			dstFieldValue.Set(srcFieldValue)
-		case reflect.String:
-			// Append the extension string.
-			dstFieldValue.SetString(dstFieldValue.String() +
-				srcFieldValue.String())
-		case reflect.Struct:
-			// Recursively extend the struct's fields.
-			newRecursePrefix := fmt.Sprintf("%s%s.", recursePrefix, strings.ToLower(field.Name))
-			a.extendPropertiesRecursive(ctx, variationType, variationName,
-				dstFieldValue, srcFieldValue,
-				newRecursePrefix)
-		case reflect.Slice:
-			dstFieldValue.Set(reflect.AppendSlice(dstFieldValue, srcFieldValue))
-		case reflect.Ptr, reflect.Interface:
-			// Recursively extend the pointed-to struct's fields.
-			if dstFieldValue.IsNil() != srcFieldValue.IsNil() {
-				panic(fmt.Errorf("can't extend field %q: nilitude mismatch"))
-			}
-			if dstFieldValue.Type() != srcFieldValue.Type() {
-				panic(fmt.Errorf("can't extend field %q: type mismatch"))
-			}
-			if !dstFieldValue.IsNil() {
-				newRecursePrefix := fmt.Sprintf("%s.%s", recursePrefix, field.Name)
-				a.extendPropertiesRecursive(ctx, variationType, variationName,
-					dstFieldValue.Elem(), srcFieldValue.Elem(),
-					newRecursePrefix)
-			}
-		default:
-			panic(fmt.Errorf("unexpected kind for property struct field %q: %s",
-				field.Name, srcFieldValue.Kind()))
-		}
-	}
-}
diff --git a/common/extend.go b/common/extend.go
new file mode 100644
index 0000000..f2d061b
--- /dev/null
+++ b/common/extend.go
@@ -0,0 +1,152 @@
+// Copyright 2015 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package common
+
+import (
+	"fmt"
+	"reflect"
+	"strings"
+
+	"github.com/google/blueprint"
+	"github.com/google/blueprint/proptools"
+)
+
+// TODO: move this to proptools
+func extendProperties(ctx blueprint.EarlyMutatorContext,
+	requiredTag, srcPrefix string, dstValues []reflect.Value, srcValue reflect.Value,
+	callback func(string, string)) {
+	if srcPrefix != "" {
+		srcPrefix += "."
+	}
+	extendPropertiesRecursive(ctx, requiredTag, srcValue, dstValues, srcPrefix, "", callback)
+}
+
+func extendPropertiesRecursive(ctx blueprint.EarlyMutatorContext, requiredTag string,
+	srcValue reflect.Value, dstValues []reflect.Value, srcPrefix, dstPrefix string,
+	callback func(string, string)) {
+
+	typ := srcValue.Type()
+	for i := 0; i < srcValue.NumField(); i++ {
+		srcField := typ.Field(i)
+		if srcField.PkgPath != "" {
+			// The field is not exported so just skip it.
+			continue
+		}
+
+		localPropertyName := proptools.PropertyNameForField(srcField.Name)
+		srcPropertyName := srcPrefix + localPropertyName
+		srcFieldValue := srcValue.Field(i)
+
+		if !ctx.ContainsProperty(srcPropertyName) {
+			continue
+		}
+
+		found := false
+		for _, dstValue := range dstValues {
+			dstField, ok := dstValue.Type().FieldByName(srcField.Name)
+			if !ok {
+				continue
+			}
+
+			dstFieldValue := dstValue.FieldByIndex(dstField.Index)
+
+			if srcFieldValue.Type() != dstFieldValue.Type() {
+				panic(fmt.Errorf("can't extend mismatching types for %q (%s <- %s)",
+					srcPropertyName, dstFieldValue.Type(), srcFieldValue.Type()))
+			}
+
+			dstPropertyName := dstPrefix + localPropertyName
+
+			if requiredTag != "" {
+				tag := dstField.Tag.Get("android")
+				tags := map[string]bool{}
+				for _, entry := range strings.Split(tag, ",") {
+					if entry != "" {
+						tags[entry] = true
+					}
+				}
+
+				if !tags[requiredTag] {
+					ctx.PropertyErrorf(srcPropertyName, "property can't be specific to a build variant")
+					continue
+				}
+			}
+
+			if callback != nil {
+				callback(srcPropertyName, dstPropertyName)
+			}
+
+			found = true
+
+			switch srcFieldValue.Kind() {
+			case reflect.Bool:
+				// Replace the original value.
+				dstFieldValue.Set(srcFieldValue)
+			case reflect.String:
+				// Append the extension string.
+				dstFieldValue.SetString(dstFieldValue.String() +
+					srcFieldValue.String())
+			case reflect.Slice:
+				dstFieldValue.Set(reflect.AppendSlice(dstFieldValue, srcFieldValue))
+			case reflect.Interface:
+				if dstFieldValue.IsNil() != srcFieldValue.IsNil() {
+					panic(fmt.Errorf("can't extend field %q: nilitude mismatch", srcPropertyName))
+				}
+				if dstFieldValue.IsNil() {
+					continue
+				}
+
+				dstFieldValue = dstFieldValue.Elem()
+				srcFieldValue = srcFieldValue.Elem()
+
+				if dstFieldValue.Type() != srcFieldValue.Type() {
+					panic(fmt.Errorf("can't extend field %q: type mismatch", srcPropertyName))
+				}
+				if srcFieldValue.Kind() != reflect.Ptr {
+					panic(fmt.Errorf("can't extend field %q: interface not a pointer", srcPropertyName))
+				}
+				fallthrough
+			case reflect.Ptr:
+				if dstFieldValue.IsNil() != srcFieldValue.IsNil() {
+					panic(fmt.Errorf("can't extend field %q: nilitude mismatch", srcPropertyName))
+				}
+				if dstFieldValue.IsNil() {
+					continue
+				}
+
+				dstFieldValue = dstFieldValue.Elem()
+				srcFieldValue = srcFieldValue.Elem()
+
+				if dstFieldValue.Type() != srcFieldValue.Type() {
+					panic(fmt.Errorf("can't extend field %q: type mismatch", srcPropertyName))
+				}
+				if srcFieldValue.Kind() != reflect.Struct {
+					panic(fmt.Errorf("can't extend field %q: pointer not to a struct", srcPropertyName))
+				}
+				fallthrough
+			case reflect.Struct:
+				// Recursively extend the struct's fields.
+				extendPropertiesRecursive(ctx, requiredTag, srcFieldValue, []reflect.Value{dstFieldValue},
+					srcPropertyName+".", srcPropertyName+".", callback)
+			default:
+				panic(fmt.Errorf("unexpected kind for property struct field %q: %s",
+					srcPropertyName, srcFieldValue.Kind()))
+			}
+		}
+		if !found {
+			ctx.PropertyErrorf(srcPropertyName, "failed to find property to extend")
+		}
+	}
+}
diff --git a/common/module.go b/common/module.go
index e8c4a87..feaba83 100644
--- a/common/module.go
+++ b/common/module.go
@@ -125,7 +125,7 @@
 	base.module = m
 	base.extendedProperties = make(map[string]struct{})
 
-	propertyStructs = append(propertyStructs, &base.commonProperties)
+	propertyStructs = append(propertyStructs, &base.commonProperties, &base.variableProperties)
 
 	return m, propertyStructs
 }
@@ -194,6 +194,7 @@
 	module AndroidModule
 
 	commonProperties        commonProperties
+	variableProperties      variableProperties
 	hostAndDeviceProperties hostAndDeviceProperties
 	generalProperties       []interface{}
 	archProperties          []*archProperties
diff --git a/common/variable.go b/common/variable.go
new file mode 100644
index 0000000..19ec625
--- /dev/null
+++ b/common/variable.go
@@ -0,0 +1,141 @@
+// Copyright 2015 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package common
+
+import (
+	"fmt"
+	"reflect"
+	"strings"
+
+	"android/soong"
+
+	"github.com/google/blueprint"
+	"github.com/google/blueprint/proptools"
+)
+
+func init() {
+	soong.RegisterEarlyMutator("variable", VariableMutator)
+}
+
+type variableProperties struct {
+	Product_variables struct {
+		Device_uses_logd struct {
+			Cflags []string
+			Srcs   []string
+		}
+		Device_uses_dlmalloc struct {
+			Cflags []string
+			Srcs   []string
+		}
+		Device_uses_jemalloc struct {
+			Cflags            []string
+			Srcs              []string
+			Whole_static_libs []string
+			Include_dirs      []string
+		}
+		Dlmalloc_alignment struct {
+			Cflags []string
+		}
+	}
+}
+
+var zeroProductVariables variableProperties
+
+// TODO: replace hardcoded test values with per-product values
+var productVariables = map[string]interface{}{
+	"device_uses_logd":     true,
+	"device_uses_jemalloc": true,
+}
+
+func VariableMutator(mctx blueprint.EarlyMutatorContext) {
+	var module AndroidModule
+	var ok bool
+	if module, ok = mctx.Module().(AndroidModule); !ok {
+		return
+	}
+
+	// TODO: depend on config variable, create variants, propagate variants up tree
+	a := module.base()
+	variableValues := reflect.ValueOf(a.variableProperties.Product_variables)
+	zeroValues := reflect.ValueOf(zeroProductVariables.Product_variables)
+
+	for i := 0; i < variableValues.NumField(); i++ {
+		variableValue := variableValues.Field(i)
+		zeroValue := zeroValues.Field(i)
+		if reflect.DeepEqual(variableValue, zeroValue) {
+			continue
+		}
+
+		name := proptools.PropertyNameForField(variableValues.Type().Field(i).Name)
+		property := "product_variables." + name
+		val := productVariables[name]
+
+		if mctx.ContainsProperty(property) && val != nil {
+			a.setVariableProperties(mctx, property, variableValue, val)
+		}
+	}
+}
+
+func (a *AndroidModuleBase) setVariableProperties(ctx blueprint.EarlyMutatorContext,
+	prefix string, productVariablePropertyValue reflect.Value, variableValue interface{}) {
+
+	generalPropertyValues := make([]reflect.Value, len(a.generalProperties))
+	for i := range a.generalProperties {
+		generalPropertyValues[i] = reflect.ValueOf(a.generalProperties[i]).Elem()
+	}
+
+	if variableValue != nil {
+		printfIntoProperties(productVariablePropertyValue, variableValue)
+	}
+
+	extendProperties(ctx, "", prefix, generalPropertyValues, productVariablePropertyValue, nil)
+}
+
+func printfIntoProperties(productVariablePropertyValue reflect.Value, variableValue interface{}) {
+	for i := 0; i < productVariablePropertyValue.NumField(); i++ {
+		propertyValue := productVariablePropertyValue.Field(i)
+		switch propertyValue.Kind() {
+		case reflect.String:
+			printfIntoProperty(propertyValue, variableValue)
+		case reflect.Slice:
+			for j := 0; j < propertyValue.Len(); j++ {
+				printfIntoProperty(propertyValue.Index(j), variableValue)
+			}
+		case reflect.Struct:
+			printfIntoProperties(propertyValue, variableValue)
+		default:
+			panic(fmt.Errorf("unsupported field kind %q", propertyValue.Kind()))
+		}
+	}
+}
+
+func printfIntoProperty(propertyValue reflect.Value, variableValue interface{}) {
+	s := propertyValue.String()
+	// For now, we only support int formats
+	var i int
+	if strings.Contains(s, "%d") {
+		switch v := variableValue.(type) {
+		case int:
+			i = v
+		case bool:
+			if v {
+				i = 1
+			}
+		default:
+			panic(fmt.Errorf("unsupported type %T", variableValue))
+		}
+		propertyValue.Set(reflect.ValueOf(fmt.Sprintf(s, i)))
+	}
+}