| package dkms | |
| import ( | |
| "fmt" | |
| "os" | |
| "regexp" | |
| "sort" | |
| "strconv" | |
| "strings" | |
| "cos.googlesource.com/cos/tools.git/src/pkg/utils" | |
| "github.com/golang/glog" | |
| ) | |
| // Config is the set of configurations for a DKMS package. | |
| // See ParseConfig for details on how to write a dkms.conf file to | |
| // populate these values. | |
| type Config struct { | |
| // The name of the package. | |
| PackageName string | |
| // The version of the package. | |
| PackageVersion string | |
| // The make command which will be used to compile all of the | |
| // modules in the package. | |
| MakeCommand string | |
| // A list of patches to apply to the package before it is built. | |
| // This is constructed from the PATCH[#] and PATCH_MATCH[#] arrays. | |
| Patches []string | |
| // The list of Modules in the package. | |
| Modules []Module | |
| // Whether or not the package should be automatically installed when | |
| // the kernel is upgraded. | |
| Autoinstall bool | |
| // A list of packages which should be built before this one. | |
| // Currently, this is only an annotation and does not affect the | |
| // build order. | |
| BuildDepends []string | |
| // A regex which specifies the kernels for which this package can | |
| // be built. | |
| BuildExclusiveKernel string | |
| // A regex which specifies which arches for which this package can | |
| // be built. | |
| BuildExclusiveArch string | |
| // The name of a script to run after adding the package.. | |
| PostAdd string | |
| // The name of a script to run after building the package. | |
| PostBuild string | |
| // The name of a script to run after installing the package. | |
| PostInstall string | |
| // The name of a script to run after removing the package. | |
| PostRemove string | |
| // The name of a script to run before building the package. | |
| PreBuild string | |
| // The name of a script to run before installing the package. | |
| PreInstall string | |
| } | |
| // LoadConfig reads a dkms.conf file for a package and parses its contents. | |
| // See ParseConfig for more details. | |
| func LoadConfig(pkg *Package) (*Config, error) { | |
| configPath := pkg.ConfigPath() | |
| var contents []byte | |
| var err error | |
| contents, err = os.ReadFile(configPath) | |
| if err != nil { | |
| if os.IsNotExist(err) { | |
| glog.V(2).Infof("could not find dkms.conf at %s; using default values", configPath) | |
| } else { | |
| return nil, err | |
| } | |
| } | |
| configText := string(contents) | |
| config, err := ParseConfig(configText, pkg) | |
| if err != nil { | |
| return nil, err | |
| } | |
| return config, nil | |
| } | |
| // ParseConfig parses the contents of a dkms.conf file for a package, | |
| // applying defaults where required. | |
| // | |
| // The defaults are assigned as bash variables in a string which is | |
| // prepended to the contents of dkms.conf. The resulting script is then | |
| // sourced, and the shell variables after executing the script are | |
| // collected into a Config. | |
| // | |
| // In addition, the following DKMS variables are available as bash | |
| // variables in dkms.conf: | |
| // module: The package name (--module from the command line) | |
| // module_version: The package version (--module-version from the command line) | |
| // arch: The target architecture for the package. | |
| // build_id: The build id of the COS kernel for the package. | |
| // board: The board of the COS kernel for the package. | |
| // kernelver: The version of the kernel for the package. | |
| // dkms_tree: The path to the DKMS tree. | |
| // source_tree: The path to the user source tree. | |
| // kernel_source_dir: The path to the directory containing the COS kernel sources. | |
| // build_dir: The path to the directory where the package will be built. | |
| func ParseConfig(contents string, pkg *Package) (*Config, error) { | |
| trees := pkg.Trees | |
| buildDir := pkg.BuildDir() | |
| // To quote the original DKMS authors: 'This is really ugly, but a neat hack'. | |
| // By declaring the array variables as arrays initially, bash will always | |
| // treat them as arrays, even if a normal assignment happens to the variable | |
| // later. Thus, a user can assign to them and read from them as if they were | |
| // either arrays or normal variables in their dkms.conf, but we can always | |
| // read their values as array elements. | |
| defaults := map[string]string{ | |
| "module": pkg.Name, | |
| "module_version": pkg.Version, | |
| "arch": pkg.Arch, | |
| "build_id": pkg.BuildId, | |
| "board": pkg.Board, | |
| "kernelver": pkg.KernelVersion, | |
| "dkms_tree": trees.Dkms, | |
| "source_tree": trees.Source, | |
| "kernel_source_dir": trees.Kernel, | |
| "build_dir": buildDir, | |
| "MAKE": fmt.Sprintf("( 'make -C %s M=%s' )", trees.Kernel, buildDir), | |
| "MAKE_MATCH": "( '.*' )", | |
| "CLEAN": "", | |
| "PATCH": "()", | |
| "PATCH_MATCH": "()", | |
| "BUILT_MODULE_NAME": fmt.Sprintf("( %s )", pkg.Name), | |
| "BUILT_MODULE_LOCATION": "()", | |
| "DEST_MODULE_NAME": "()", // if this is still empty later, it will default to BUILT_MODULE_NAME | |
| "DEST_MODULE_LOCATION": "( /kernel/updates )", | |
| "STRIP": "( yes )", | |
| "BUILD_DEPENDS": "()", | |
| "PACKAGE_NAME": pkg.Name, | |
| "PACKAGE_VERSION": pkg.Version, | |
| "AUTOINSTALL": "no", | |
| "BUILD_EXCLUSIVE_KERNEL": ".*", | |
| "BUILD_EXCLUSIVE_ARCH": ".*", | |
| } | |
| var defaultsLines []string | |
| for variable, value := range defaults { | |
| defaultsLines = append(defaultsLines, fmt.Sprintf("%s=%s", variable, value)) | |
| } | |
| sort.Strings(defaultsLines) | |
| defaultsString := strings.Join(defaultsLines, "\n") | |
| configString := strings.Join([]string{defaultsString, contents}, "\n") | |
| vars, err := utils.SourceString(configString) | |
| if err != nil { | |
| return nil, err | |
| } | |
| // Get the MAKE command. | |
| makeCommands := utils.ArrayElements(vars["MAKE"]) | |
| makeMatches := utils.ArrayElements(vars["MAKE_MATCH"]) | |
| makeCommand, err := matchMakeCommand(pkg.KernelVersion, makeCommands, makeMatches) | |
| if err != nil { | |
| return nil, fmt.Errorf("error while matching MAKE command: %v", err) | |
| } | |
| // Get the set of patches to apply. | |
| patchMap := utils.ArrayElements(vars["PATCH"]) | |
| patchMatches := utils.ArrayElements(vars["PATCH_MATCH"]) | |
| patches, err := selectPatches(pkg.KernelVersion, patchMap, patchMatches) | |
| if err != nil { | |
| return nil, fmt.Errorf("error while selecting patches to apply: %v", err) | |
| } | |
| // Get the list of build dependencies. These should be kept in the numerical order in | |
| // which they appear in the BUILD_DEPENDS array. | |
| buildDependsMap := utils.ArrayElements(vars["BUILD_DEPENDS"]) | |
| var buildDependsKeys []string | |
| buildDepends := []string{} | |
| for key := range buildDependsMap { | |
| buildDependsKeys = append(buildDependsKeys, key) | |
| } | |
| err = sortStringsAsInts(buildDependsKeys) | |
| if err != nil { | |
| return nil, fmt.Errorf("error while sorting BUILD_DEPENDS keys: %v", err) | |
| } | |
| for _, key := range buildDependsKeys { | |
| buildDepends = append(buildDepends, buildDependsMap[key]) | |
| } | |
| // Get the list of modules. | |
| builtModuleNames := utils.ArrayElements(vars["BUILT_MODULE_NAME"]) | |
| builtModuleLocations := utils.ArrayElements(vars["BUILT_MODULE_LOCATION"]) | |
| destModuleNames := utils.ArrayElements(vars["DEST_MODULE_NAME"]) | |
| destModuleLocations := utils.ArrayElements(vars["DEST_MODULE_LOCATION"]) | |
| strips := utils.ArrayElements(vars["STRIP"]) | |
| modules, err := collateModules(pkg, builtModuleNames, builtModuleLocations, destModuleNames, destModuleLocations, strips) | |
| if err != nil { | |
| return nil, fmt.Errorf("error while collecting modules from config: %v", err) | |
| } | |
| autoinstall := vars["AUTOINSTALL"] == "yes" | |
| config := Config{ | |
| PackageName: vars["PACKAGE_NAME"], | |
| PackageVersion: vars["PACKAGE_VERSION"], | |
| MakeCommand: makeCommand, | |
| Patches: patches, | |
| Modules: modules, | |
| Autoinstall: autoinstall, | |
| BuildDepends: buildDepends, | |
| BuildExclusiveKernel: vars["BUILD_EXCLUSIVE_KERNEL"], | |
| BuildExclusiveArch: vars["BUILD_EXCLUSIVE_ARCH"], | |
| PostAdd: vars["POST_ADD"], | |
| PostBuild: vars["POST_BUILD"], | |
| PostInstall: vars["POST_INSTALL"], | |
| PostRemove: vars["POST_REMOVE"], | |
| PreBuild: vars["PRE_BUILD"], | |
| PreInstall: vars["PRE_INSTALL"], | |
| } | |
| return &config, nil | |
| } | |
| // matchMakeCommand returns the first make command in numerical order which matches | |
| // the provided kernel version. | |
| func matchMakeCommand(kernelVersion string, makeCommands, makeMatches map[string]string) (string, error) { | |
| if len(makeCommands) == 1 && len(makeMatches) == 0 { | |
| // return the first make command since there is only one | |
| for _, makeCommand := range makeCommands { | |
| return makeCommand, nil | |
| } | |
| } | |
| key, err := firstMatchingKey(kernelVersion, makeMatches) | |
| if err != nil { | |
| return "", fmt.Errorf("error while trying to match make commands to kernel version: %v", err) | |
| } | |
| makeCommand, ok := makeCommands[key] | |
| if !ok { | |
| return "", fmt.Errorf("kernelver matched MAKE_MATCH[%s], but no corresponding MAKE found", key) | |
| } | |
| return makeCommand, nil | |
| } | |
| // selectPatches returns the list of patches in numerical order which match the | |
| // provided kernel version. | |
| func selectPatches(kernelVersion string, patchMap, patchMatches map[string]string) ([]string, error) { | |
| var keys []string | |
| for key := range patchMap { | |
| keys = append(keys, key) | |
| } | |
| err := sortStringsAsInts(keys) | |
| if err != nil { | |
| return nil, fmt.Errorf("invalid keys in patterns array: %v", err) | |
| } | |
| patches := []string{} | |
| for _, key := range keys { | |
| pattern, ok := patchMatches[key] | |
| // If there's a pattern, check the kernel version and skip the patch | |
| // if it doesn't match. If there's no pattern, we always apply the patch. | |
| if ok { | |
| regex, err := regexp.Compile(pattern) | |
| if err != nil { | |
| return nil, fmt.Errorf("could not compile regexp %s for key %s", pattern, key) | |
| } | |
| if !regex.MatchString(kernelVersion) { | |
| continue | |
| } | |
| } | |
| patch, ok := patchMap[key] | |
| if !ok { | |
| return nil, fmt.Errorf("kernelver matched PATCH_MATCH[%s], but no corresponding PATCH found", key) | |
| } | |
| patches = append(patches, patch) | |
| } | |
| return patches, nil | |
| } | |
| // firstMatchingKey takes a mapping from keys to patterns and returns the first key | |
| // in ascending numerical order whose corresponding pattern matches a target string. | |
| // Errors if any key cannot be converted to an integer or if any of the patterns | |
| // cannot be compiled as a regexp. | |
| func firstMatchingKey(text string, patterns map[string]string) (string, error) { | |
| var keys []string | |
| for key := range patterns { | |
| keys = append(keys, key) | |
| } | |
| err := sortStringsAsInts(keys) | |
| if err != nil { | |
| return "", fmt.Errorf("invalid keys in patterns array: %v", err) | |
| } | |
| var regexes []*regexp.Regexp | |
| for _, key := range keys { | |
| pattern := patterns[key] | |
| regex, err := regexp.Compile(pattern) | |
| if err != nil { | |
| return "", fmt.Errorf("could not compile regexp %s for key %s", pattern, key) | |
| } | |
| regexes = append(regexes, regex) | |
| } | |
| for i, key := range keys { | |
| if regexes[i].MatchString(text) { | |
| return key, nil | |
| } | |
| } | |
| return "", fmt.Errorf("no matching patterns found for text %s", text) | |
| } | |
| // sortStringsAsInts sorts a list of strings in ascending numerical order. | |
| // Returns an error if any of the strings cannot be converted to an int. | |
| func sortStringsAsInts(strings []string) error { | |
| var ints []int | |
| for _, s := range strings { | |
| intValue, err := strconv.Atoi(s) | |
| if err != nil { | |
| return fmt.Errorf("error converting %s to int when sorting strings: %v", s, err) | |
| } | |
| ints = append(ints, intValue) | |
| } | |
| sort.Ints(ints) | |
| for i, intValue := range ints { | |
| strings[i] = strconv.Itoa(intValue) | |
| } | |
| return nil | |
| } | |
| // collateModules takes the module-related arrays from dkms.conf and collates them to | |
| // construct a list of Modules. | |
| // This gathers the full set of keys from all of the arrays and sorts them in | |
| // numeric order before iterating, which ensures that the output is deterministic. | |
| func collateModules(pkg *Package, builtNames, builtLocations, destNames, destLocations, strips map[string]string) ([]Module, error) { | |
| moduleKeys := allKeys([]map[string]string{ | |
| builtNames, | |
| builtLocations, | |
| destNames, | |
| destLocations, | |
| strips, | |
| }) | |
| err := sortStringsAsInts(moduleKeys) | |
| if err != nil { | |
| return nil, fmt.Errorf("invalid keys in module arrays: %v", err) | |
| } | |
| var modules []Module | |
| for _, key := range moduleKeys { | |
| builtName := builtNames[key] | |
| builtLocation := builtLocations[key] | |
| destName := destNames[key] | |
| if destName == "" { | |
| destName = builtName | |
| } | |
| destLocation := destLocations[key] | |
| if destLocation == "" { | |
| destLocation = destLocations[moduleKeys[0]] | |
| } | |
| if !strings.HasPrefix(destLocation, "/kernel") { | |
| return nil, fmt.Errorf("DEST_MODULE_LOCATION %s must begin with /kernel", destLocation) | |
| } | |
| strip := strips[key] | |
| if strip == "" { | |
| strip = strips[moduleKeys[0]] | |
| } | |
| modules = append(modules, Module{ | |
| Package: pkg, | |
| BuiltName: builtName, | |
| BuiltLocation: builtLocation, | |
| DestName: destName, | |
| DestLocation: destLocation, | |
| Strip: strip != "no", | |
| }) | |
| } | |
| if len(modules) == 0 { | |
| return nil, fmt.Errorf("package must contain at least one module, found 0") | |
| } | |
| return modules, nil | |
| } | |
| // allKeys returns the set of all keys from a list of maps. | |
| // For example, allKeys([]map[string]int {{"a": "1", "b": "2"}, {"a": "3", "c": "4"}}) | |
| // will output []string{"a", "b", "c"} (though the order of keys is not guaranteed). | |
| func allKeys(maps []map[string]string) []string { | |
| keySet := make(map[string]bool) | |
| for _, items := range maps { | |
| for key := range items { | |
| keySet[key] = true | |
| } | |
| } | |
| var keys []string | |
| for key := range keySet { | |
| keys = append(keys, key) | |
| } | |
| return keys | |
| } |