install/ — 超深度逐行分析

四个文件:main.go (128行) + common.go (33行) + constant.go (87行) + docker_process.go (215行) + containerd_process.go (272行) = 735行
核心职责:安装/卸载ascend-docker-runtime,自动修改Docker或Containerd配置文件


一、模块定位

1.1 业务职责

install/ 模块是 ascend-docker-runtime 的安装辅助工具。它负责在安装或卸载时自动修改容器引擎的配置文件:

  1. Docker场景 — 修改 /etc/docker/daemon.json,添加/移除ascend运行时
  2. Containerd场景 — 修改 /etc/containerd/config.toml,修改runc配置
  3. iSula场景 — 与Docker相同的处理逻辑
  4. cgroup v1/v2兼容 — 根据cgroup版本选择不同的配置策略

1.2 在系统中的位置

配置文件

安装流程

docker/isula

containerd

安装脚本 build/install.sh

install/main.go

场景判断

DockerProcess 修改daemon.json

ContainerdProcess 修改config.toml

写入daemon.json

写入config.toml

/etc/docker/daemon.json (runtimes.ascend配置)

/etc/containerd/config.toml (runc.BinaryName/runtime_type)


二、install/main.go 逐行解析(128行)

2.1 模板定义

const commonTemplate = `{
        "runtimes":     {
                "ascend":       {
                        "path": "%s",
                        "runtimeArgs":  []
                }
        },
        "default-runtime":      "ascend"
}`

const noDefaultTemplate = `{
        "runtimes":     {
                "ascend":       {
                        "path": "%s",
                        "runtimeArgs":  []
                }
        }
}`
模板 用途
commonTemplate 创建新daemon.json时使用,设置ascend为默认运行时
noDefaultTemplate 创建新daemon.json但不修改默认运行时(保留docker默认runc)

设计意图:当daemon.json不存在时,用模板直接生成。%s是ascend-docker-runtime二进制路径。

2.2 常量

const (
    maxCommandLength         = 65535
    logPath                  = api.InstallHelperRunLogPath
    minCommandLength         = 2
    installSceneIndexFromEnd = 4
)

var reserveDefaultRuntime = false
常量 用途
maxCommandLength 参数最大长度
logPath 安装日志路径
installSceneIndexFromEnd 场景参数从末尾算的索引位置(第4个)
reserveDefaultRuntime 是否保留原有默认runtime(不强制改为ascend)

2.3 main函数

func main() {
    ctx, _ := context.WithCancel(context.Background())
    if err := initLogModule(ctx); err != nil {
        log.Fatal(err)
    }
    logPrefixWords, err := mindxcheckutils.GetLogPrefix()
    if err != nil {
        log.Fatal(err)
    }
    hwlog.RunLog.Infof("%v start running script", logPrefixWords)

与其他入口文件相同的模式:创建context → 初始化日志 → 获取日志前缀。

    if !mindxcheckutils.StringChecker(strings.Join(os.Args, " "), 0,
        maxCommandLength, mindxcheckutils.DefaultWhiteList+" ") {
        hwlog.RunLog.Errorf("%v check command failed, maybe command contains illegal char", logPrefixWords)
        log.Fatalf("command error, please check %s for detail", logPath)
    }

参数安全校验:与runtime/main.go和hook/main.go一致,StringChecker白名单过滤。

    const helpMessage = "\tadd <config file path> <new config file path> " +
        "<docker-runtime path> <whether reserve default> <docker or containerd> <cgroup info> <os name> <os version>\n" +
        "\t rm <config file path> <new config file path> <docker or containerd> <whether reserve default>" +
        " <docker or containerd> <cgroup info> <os name> <os version>\n" + "\t -h help command"
    helpFlag := flag.Bool("h", false, helpMessage)
    flag.Parse()
    if *helpFlag {
        _, err := fmt.Println(helpMessage)
        log.Fatalf("need help, error: %v", err)
    }

命令行格式

install add <src> <dst> <runtime_path> <reserve_default> <scene> <cgroup> <os_name> <os_version>
install rm  <src> <dst> <runtime_path> <reserve_default> <scene> <cgroup> <os_name> <os_version>
参数位置 add命令 rm命令 说明
0 add rm 操作类型
1 源配置文件路径 源配置文件路径 原始daemon.json/config.toml
2 目标配置文件路径 目标配置文件路径 修改后的配置文件输出路径
3 runtime二进制路径 (不用) ascend-docker-runtime路径
4 是否保留默认runtime 是否保留默认runtime “yes"或"no”
5 场景 场景 “docker”/“containerd”/“isula”
6 cgroup信息 cgroup信息 "cgroup2fs"或其他
7 OS名称 OS名称 如"openEuler"
8 OS版本 OS版本 如"24.03"

add命令9个参数(addCommandLength=9),rm命令8个参数(rmCommandLength=8)。

    command := flag.Args()
    if len(command) == 0 {
        log.Fatalf("error param")
    }
    var behavior string
    if !process.CheckParamLength(command) {
        log.Fatalf("error param")
    }
    installScene := command[len(command)-installSceneIndexFromEnd]
    if installScene == process.InstallSceneDocker || installScene == process.InstallSceneIsula {
        behavior, err = process.DockerProcess(command)
    } else if installScene == process.InstallSceneContainerd {
        behavior, err = process.ContainerdProcess(command)
    } else {
        log.Fatalf("error param: %v", command[len(command)-1])
    }
    if err != nil {
        hwlog.RunLog.Errorf("%v run script failed: %v", logPrefixWords, err)
        log.Fatal(fmt.Errorf("error in installation, err is %v", err))
    }
    hwlog.RunLog.Infof("%v run %v success", logPrefixWords, behavior)
}

逐行解析

  1. flag.Args() — 获取非flag参数(即add/rm及其参数)
  2. CheckParamLength — 校验参数数量(add=9, rm=8)
  3. installScene — 从末尾第4个参数提取场景(docker/containerd/isula)
  4. 根据场景分发:
    • docker/isula → DockerProcess
    • containerd → ContainerdProcess
  5. 记录成功日志

docker/isula

containerd

其他

install main

日志初始化

参数安全校验

解析flag参数

参数数量合法?

log.Fatal error param

提取installScene
倒数第4个参数

场景类型?

DockerProcess

ContainerdProcess

log.Fatal error param

成功?

log.Fatal

记录成功日志


三、install/process/constant.go 逐行解析(87行)

3.1 模板常量(与main.go中重复定义)

const commonTemplate = `...`
const noDefaultTemplate = `...`

注意:这些模板在main.go和constant.go中重复定义。这是代码冗余,可能是因为main.go和process包需要分别使用。

3.2 位置索引常量

const (
    reserveIndexFromEnd = 5
    actionPosition      = 0
    srcFilePosition     = 1
    destFilePosition    = 2
    runtimeFilePosition = 3
    rmCommandLength     = 8
    addCommandLength    = 9
    maxFileSize         = 1024 * 1024 * 10  // 10MB
    cgroupInfoIndexFromEnd   = 3
    osNameIndexFromEnd       = 2
    osVersionIndexFromEnd    = 1
    perm               os.FileMode = 0600
)

参数位置映射

command[0] = action (add/rm)
command[1] = srcFilePath (源配置文件)
command[2] = destFilePath (目标配置文件)
command[3] = runtimeFilePath (runtime二进制路径,仅add)

从末尾索引的参数

command[len-1] = osVersion
command[len-2] = osName
command[len-3] = cgroupInfo
command[len-4] = installScene (docker/containerd/isula)
command[len-5] = reserveDefault (yes/no)

3.3 配置键常量

const (
    addCommand        = "add"
    rmCommand         = "rm"
    defaultRuntimeKey = "default-runtime"
    InstallSceneDocker     = "docker"
    InstallSceneContainerd = "containerd"
    InstallSceneIsula      = "isula"
    
    v1NeedChangeKeyRuntime     = "runtime"
    v1NeedChangeKeyRuntimeType = "runtime_type"
    v1RuntimeType              = "io.containerd.runtime.v1.linux"
    v2RuncRuntimeType          = "io.containerd.runc.v2"
    defaultRuntimeValue        = "runc"
    
    v1RuntimeTypeFirstLevelPlugin = "io.containerd.grpc.v1.cri"
    containerdKey                 = "containerd"
    runtimesKey                   = "runtimes"
    runcKey                       = "runc"
    runcOptionsKey                = "options"
    binaryNameKey                 = "BinaryName"
    
    cgroupV2InfoStr                  = "cgroup2fs"
    openEulerStr                     = "openEuler"
    openEulerVersionForV2RuntimeType = "24.03"
)
常量 用途
v1RuntimeTypeFirstLevelPlugin containerd的CRI插件key
v1RuntimeType containerd V1运行时类型(cgroup v1用)
v2RuncRuntimeType containerd V2 runc运行时类型
binaryNameKey containerd配置中BinaryName字段名
cgroupV2InfoStr cgroup v2的文件系统标识
openEulerVersionForV2RuntimeType openEuler 24.03使用V2运行时类型

四、install/process/common.go 逐行解析(33行)

func checkParamAndGetBehavior(action string, command []string) (bool, string) {
    correctParam, behavior := false, ""
    if action == addCommand && len(command) == addCommandLength {
        correctParam = true
        behavior = "install"
    }
    if action == rmCommand && len(command) == rmCommandLength {
        correctParam = true
        behavior = "uninstall"
    }
    return correctParam, behavior
}

func CheckParamLength(command []string) bool {
    return len(command) == addCommandLength || len(command) == rmCommandLength
}
函数 逻辑
checkParamAndGetBehavior add+9参数→install, rm+8参数→uninstall
CheckParamLength 参数数量必须正好是9(add)或8(rm)

五、install/process/docker_process.go 逐行解析(215行)

5.1 DockerProcess — 主入口

func DockerProcess(command []string) (string, error) {
    if len(command) == 0 {
        return "", fmt.Errorf("error param, length of command is 0")
    }
    action := command[actionPosition]
    correctParam, behavior := checkParamAndGetBehavior(action, command)
    if !correctParam {
        return "", fmt.Errorf("error param")
    }

    srcFilePath := command[srcFilePosition]
    if _, err := os.Stat(srcFilePath); os.IsNotExist(err) {
        if _, err := mindxcheckutils.RealDirChecker(filepath.Dir(srcFilePath), true, false); err != nil {
            return behavior, err
        }
    } else {
        if _, err := mindxcheckutils.RealFileChecker(srcFilePath, true, false, mindxcheckutils.DefaultSize); err != nil {
            return behavior, err
        }
    }

    destFilePath := command[destFilePosition]
    if _, err := mindxcheckutils.RealDirChecker(filepath.Dir(destFilePath), true, false); err != nil {
        return behavior, err
    }
    runtimeFilePath := ""
    if len(command) == addCommandLength {
        runtimeFilePath = command[runtimeFilePosition]
        if _, err := mindxcheckutils.RealFileChecker(runtimeFilePath, true, false, mindxcheckutils.DefaultSize); err != nil {
            return behavior, err
        }
    }

    setReserveDefaultRuntime(command)

    writeContent, err := createJsonString(srcFilePath, runtimeFilePath, action)
    if err != nil {
        return behavior, err
    }
    return behavior, writeJson(destFilePath, writeContent)
}

逐行解析

  1. 提取action(add/rm)
  2. 参数校验
  3. 源文件校验
    • 如果源文件不存在 → 校验其父目录存在(新建文件场景)
    • 如果源文件存在 → 安全校验文件
  4. 目标文件校验:校验目标文件父目录存在
  5. runtime路径校验(仅add命令):安全校验runtime二进制
  6. setReserveDefaultRuntime — 解析是否保留默认runtime
  7. createJsonString — 生成修改后的JSON内容
  8. writeJson — 写入目标文件

DockerProcess

提取action

参数校验

校验源文件
存在→安全检查
不存在→检查父目录

校验目标文件父目录

action=add?

校验runtime二进制路径

跳过

setReserveDefaultRuntime
解析reserve参数

createJsonString
生成配置JSON

writeJson
写入目标文件

5.2 createJsonString — 生成配置JSON

func createJsonString(srcFilePath, runtimeFilePath, action string) ([]byte, error) {
    var writeContent []byte
    if _, err := os.Stat(srcFilePath); err == nil {
        // 源文件已存在 → 修改现有配置
        daemon, err := modifyDaemon(srcFilePath, runtimeFilePath, action)
        if err != nil {
            return nil, err
        }
        writeContent, err = json.MarshalIndent(daemon, "", "        ")
        if err != nil {
            return nil, err
        }
    } else if os.IsNotExist(err) {
        // 源文件不存在 → 用模板创建新配置
        if !reserveDefaultRuntime {
            writeContent = []byte(fmt.Sprintf(commonTemplate, runtimeFilePath))
        } else {
            writeContent = []byte(fmt.Sprintf(noDefaultTemplate, runtimeFilePath))
        }
    } else {
        return nil, err
    }
    return writeContent, nil
}

三分支逻辑

情况 处理
源文件存在 modifyDaemon修改现有JSON
源文件不存在 + 不保留默认 用commonTemplate(设ascend为默认)
源文件不存在 + 保留默认 用noDefaultTemplate(不设默认)

5.3 modifyDaemon — 修改现有daemon.json

func modifyDaemon(srcFilePath, runtimeFilePath, action string) (map[string]interface{}, error) {
    daemon, err := loadOriginJson(srcFilePath)
    if err != nil {
        return nil, err
    }

    if _, ok := daemon["runtimes"]; !ok && action == addCommand {
        daemon["runtimes"] = map[string]interface{}{}
    }
    runtimeValue := daemon["runtimes"]
    runtimeConfig, runtimeConfigOk := runtimeValue.(map[string]interface{})
    if !runtimeConfigOk && action == addCommand {
        return nil, fmt.Errorf("extract runtime failed")
    }
    if action == addCommand {
        runtimeConfig, daemon, err = addDockerDaemon(runtimeConfig, daemon, runtimeFilePath)
    } else if action == rmCommand {
        runtimeConfig, daemon, err = rmDockerDaemon(runtimeConfig, daemon, runtimeConfigOk)
    }
    return daemon, err
}

逐行解析

  1. loadOriginJson — 读取并解析原始daemon.json
  2. 如果"runtimes"键不存在且是add操作 → 创建空的runtimes map
  3. 类型断言提取runtimeConfig
  4. add → addDockerDaemon;rm → rmDockerDaemon

5.4 addDockerDaemon / rmDockerDaemon

func addDockerDaemon(runtimeConfig, daemon map[string]interface{}, runtimeFilePath string,
) (map[string]interface{}, map[string]interface{}, error) {
    if runtimeConfig == nil {
        return nil, daemon, fmt.Errorf("runtime config is nil")
    }
    if _, ok := runtimeConfig["ascend"]; !ok {
        runtimeConfig["ascend"] = map[string]interface{}{}
    }
    ascendConfig, ok := runtimeConfig["ascend"].(map[string]interface{})
    if !ok {
        return nil, nil, fmt.Errorf("extract ascend failed")
    }
    ascendConfig["path"] = runtimeFilePath
    if _, ok := ascendConfig["runtimeArgs"]; !ok {
        ascendConfig["runtimeArgs"] = []string{}
    }
    if !reserveDefaultRuntime && daemon != nil {
        daemon[defaultRuntimeKey] = "ascend"
    }
    return runtimeConfig, daemon, nil
}

add操作生成的JSON结构

{
    "runtimes": {
        "ascend": {
            "path": "/usr/local/bin/ascend-docker-runtime",
            "runtimeArgs": []
        }
    },
    "default-runtime": "ascend"
}
func rmDockerDaemon(runtimeConfig, daemon map[string]interface{}, runtimeConfigOk bool,
) (map[string]interface{}, map[string]interface{}, error) {
    if runtimeConfigOk {
        delete(runtimeConfig, "ascend")
    }
    if value, ok := daemon[defaultRuntimeKey]; ok && value == "ascend" {
        delete(daemon, defaultRuntimeKey)
    }
    return runtimeConfig, daemon, nil
}

rm操作:删除"ascend"运行时配置 + 如果默认runtime是ascend则删除默认runtime设置。

5.5 setReserveDefaultRuntime

func setReserveDefaultRuntime(command []string) {
    reserveCmdPostion := len(command) - reserveIndexFromEnd
    if reserveCmdPostion >= len(command) || reserveCmdPostion < 0 {
        return
    }
    if command[reserveCmdPostion] == "yes" {
        reserveDefaultRuntime = true
    }
}
  • 从命令行参数倒数第5个提取reserve标志
  • “yes” → 保留原有默认runtime(不强制改为ascend)

六、install/process/containerd_process.go 逐行解析(272行)

6.1 commandArgs 结构体

type commandArgs struct {
    action          string
    srcFilePath     string
    runtimeFilePath string
    destFilePath    string
    cgroupInfo      string
    osName          string
    osVersion       string
}

commandArgs

+string action "add/rm"

+string srcFilePath "源config.toml"

+string runtimeFilePath "runtime二进制路径"

+string destFilePath "目标config.toml"

+string cgroupInfo "cgroup版本信息"

+string osName "OS名称"

+string osVersion "OS版本"

6.2 ContainerdProcess — 主入口

func ContainerdProcess(command []string) (string, error) {
    // ...参数校验同DockerProcess...
    
    arg := &commandArgs{
        action:          action,
        srcFilePath:     srcFilePath,
        runtimeFilePath: runtimeFilePath,
        destFilePath:    destFilePath,
        cgroupInfo:      command[len(command)-cgroupInfoIndexFromEnd],
        osName:          command[len(command)-osNameIndexFromEnd],
        osVersion:       command[len(command)-osVersionIndexFromEnd],
    }
    err := editContainerdConfig(arg)
    // ...
    return behavior, nil
}

与DockerProcess的区别

  • Docker修改daemon.json(JSON格式)
  • Containerd修改config.toml(TOML格式)
  • Containerd需要额外参数:cgroupInfo、osName、osVersion

6.3 editContainerdConfig — 核心配置编辑

func editContainerdConfig(arg *commandArgs) error {
    if arg == nil {
        return errors.New("arg is nil")
    }
    cfg := config.Config{}
    if err := config.LoadConfig(arg.srcFilePath, &cfg); err != nil {
        return err
    }
    if strings.Contains(arg.cgroupInfo, cgroupV2InfoStr) {
        hwlog.RunLog.Info("it is cgroup v2")
        binaryName := ""
        if arg.action == addCommand {
            binaryName = arg.runtimeFilePath
        }
        err := changeCgroupV2BinaryNameConfig(&cfg, binaryName)
        if err != nil {
            return err
        }
    } else {
        hwlog.RunLog.Info("it is cgroup v1")
        runtimeValue := defaultRuntimeValue
        runtimeType := v2RuncRuntimeType
        if arg.action == addCommand {
            runtimeValue = arg.runtimeFilePath
            runtimeType = v1RuntimeType
            if arg.osName == openEulerStr && arg.osVersion == openEulerVersionForV2RuntimeType {
                runtimeType = v2RuncRuntimeType
            }
        }
        err := changeCgroupV1Config(&cfg, runtimeValue, runtimeType)
        if err != nil {
            return err
        }
    }
    err := writeContainerdConfigToFile(cfg, arg.destFilePath)
    return err
}

cgroup v1 vs v2 配置策略

维度 cgroup v1 cgroup v2
修改位置 plugins.io.containerd.runtime.v1.linux plugins.io.containerd.grpc.v1.cri.containerd.runtimes.runc.options
修改字段 runtime(路径)+ runtime_type BinaryName(路径)
add操作 runtime=ascend路径, runtime_type=v1.linux BinaryName=ascend路径
rm操作 runtime=runc, runtime_type=v2.runc BinaryName=“” (空)
openEuler 24.03特殊处理 使用v2.runc类型

cgroup2fs

其他

add

rm

add

rm

editContainerdConfig

LoadConfig
加载config.toml

cgroup版本?

cgroup v2路径

cgroup v1路径

action?

BinaryName = runtimeFilePath

BinaryName = 空

changeCgroupV2BinaryNameConfig

action?

runtimeValue = runtimeFilePath
runtimeType = v1.linux

runtimeValue = runc
runtimeType = v2.runc

openEuler 24.03?

runtimeType = v2.runc

保持v1.linux

changeCgroupV1Config

writeContainerdConfigToFile
写入config.toml

6.4 changeCgroupV2BinaryNameConfig

func changeCgroupV2BinaryNameConfig(cfg *config.Config, binaryName string) error {
    value, ok := cfg.Plugins[v1RuntimeTypeFirstLevelPlugin]
    if !ok {
        return fmt.Errorf(notFindPluginErrorStr, v1RuntimeTypeFirstLevelPlugin)
    }
    valueMap := value.ToMap()
    containerdConfig := valueMap[containerdKey]
    runtimesConfig, err := getMap(containerdConfig, runtimesKey)
    if err != nil {
        return err
    }
    runcConfig, err := getMap(runtimesConfig, runcKey)
    if err != nil {
        return err
    }
    runcOptionsConfig, err := getMap(runcConfig, runcOptionsKey)
    if err != nil {
        return err
    }
    runcOptionsConfigMap, ok := runcOptionsConfig.(map[string]interface{})
    if !ok {
        return fmt.Errorf(convertConfigFailErrorStr, runcOptionsKey, runcOptionsConfig)
    }
    runcOptionsConfigMap[binaryNameKey] = binaryName
    newTree, err := toml.TreeFromMap(valueMap)
    if err != nil {
        return err
    }
    cfg.Plugins[v1RuntimeTypeFirstLevelPlugin] = *newTree
    return nil
}

逐行解析

  1. 获取 io.containerd.grpc.v1.cri 插件配置
  2. 层层深入:containerd → runtimes → runc → options
  3. 设置 BinaryName 为ascend-docker-runtime路径(add)或空字符串(rm)
  4. 将修改后的map转回TOML Tree
  5. 更新cfg.Plugins

配置路径plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options.BinaryName

6.5 changeCgroupV1Config

func changeCgroupV1Config(cfg *config.Config, runtimeValue, runtimeType string) error {
    err := changeCgroupV1RuntimeConfig(cfg, runtimeValue)
    if err != nil {
        return err
    }
    return changeCgroupV1RuntimeTypeConfig(cfg, runtimeType)
}

两步操作

  1. changeCgroupV1RuntimeConfig — 修改 plugins."io.containerd.runtime.v1.linux".runtime 为runtime路径
  2. changeCgroupV1RuntimeTypeConfig — 修改 plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.runtime_type 为运行时类型

6.6 getMap — 安全map取值

func getMap(input interface{}, key string) (interface{}, error) {
    inputMap, ok := input.(map[string]interface{})
    if !ok {
        return nil, fmt.Errorf(convertConfigFailErrorStr, key, input)
    }
    output, ok := inputMap[key]
    if !ok {
        return nil, fmt.Errorf("can not find config: %v", key)
    }
    return output, nil
}
  • 类型断言 + 键存在性检查
  • 每一步都有错误处理,防止空指针panic

6.7 writeContainerdConfigToFile

func writeContainerdConfigToFile(cfg config.Config, destFilePath string) error {
    tomlString, err := toml.Marshal(cfg)
    if err != nil {
        return err
    }
    file, err := os.OpenFile(destFilePath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, perm)
    if err != nil {
        return err
    }
    defer func() {
        err := file.Close()
        if err != nil {
            hwlog.RunLog.Errorf("failed to close file, error: %v", err)
        }
    }()
    _, err = file.Write(tomlString)
    return err
}
  • toml.Marshal — 将Config序列化为TOML格式
  • O_CREATE|O_RDWR|O_TRUNC — 创建或截断写入
  • 文件权限0600(仅所有者读写)
  • defer关闭文件

七、完整安装/卸载流程

文件系统 ContainerdProcess DockerProcess install/main.go 安装脚本 文件系统 ContainerdProcess DockerProcess install/main.go 安装脚本 alt [add] [rm] alt [源daemon.json存在] [源daemon.json不存在] alt [cgroup v2] [cgroup v1] alt [Docker场景] [Containerd场景] add <src> <dst> <runtime> <reserve> <scene> <cgroup> <os> <ver> 参数校验 DockerProcess(command) setReserveDefaultRuntime 读取daemon.json modifyDaemon addDockerDaemon 设置runtimes.ascend.path 设置default-runtime=ascend rmDockerDaemon 删除runtimes.ascend 删除default-runtime json.MarshalIndent 用模板生成新JSON 写入新daemon.json ContainerdProcess(command) 读取config.toml editContainerdConfig changeCgroupV2BinaryNameConfig 设置BinaryName changeCgroupV1RuntimeConfig 设置runtime路径 changeCgroupV1RuntimeTypeConfig 设置runtime_type 写入config.toml 返回成功/失败
Logo

鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。

更多推荐