commit 0fe28fc0472cd667d12b33b0a52d0bd2a009fee7 Author: wheelchairy Date: Tue Feb 4 15:49:54 2025 +0300 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d267807 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +installed* +repo +train diff --git a/cmd/info.go b/cmd/info.go new file mode 100644 index 0000000..d5d9ffc --- /dev/null +++ b/cmd/info.go @@ -0,0 +1,23 @@ +package cmd + +import ( + "fmt" + "train/pkg/installer" + + "github.com/spf13/cobra" +) + +var infoCmd = &cobra.Command{ + Use: "info [package]", + Short: "Выводит информацию о пакете", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + pkgName := args[0] + info, err := installer.GetPackageInfo(pkgName) + if err != nil { + fmt.Printf("Ошибка получения информации: %v\n", err) + return + } + fmt.Printf("Информация о пакете %s:\n%s\n", pkgName, info) + }, +} diff --git a/cmd/install.go b/cmd/install.go new file mode 100644 index 0000000..df2a309 --- /dev/null +++ b/cmd/install.go @@ -0,0 +1,24 @@ +package cmd + +import ( + "fmt" + "os" + "train/pkg/installer" + + "github.com/spf13/cobra" +) + +var installCmd = &cobra.Command{ + Use: "install [package]", + Short: "Устанавливает указанный пакет", + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + pkgName := args[0] + fmt.Printf("Установка пакета: %s\n", pkgName) + if err := installer.InstallPackage(pkgName); err != nil { + fmt.Printf("Ошибка установки: %v\n", err) + os.Exit(1) + } + fmt.Printf("Пакет %s успешно установлен.\n", pkgName) + }, +} diff --git a/cmd/list.go b/cmd/list.go new file mode 100644 index 0000000..c064581 --- /dev/null +++ b/cmd/list.go @@ -0,0 +1,24 @@ +package cmd + +import ( + "fmt" + "train/pkg/installer" + + "github.com/spf13/cobra" +) + +var listCmd = &cobra.Command{ + Use: "list", + Short: "Список установленных пакетов", + Run: func(cmd *cobra.Command, args []string) { + pkgs, err := installer.ListInstalledPackages() + if err != nil { + fmt.Printf("Ошибка получения списка: %v\n", err) + return + } + fmt.Println("Установленные пакеты:") + for _, p := range pkgs { + fmt.Println(" -", p) + } + }, +} diff --git a/cmd/remove.go b/cmd/remove.go new file mode 100644 index 0000000..789876a --- /dev/null +++ b/cmd/remove.go @@ -0,0 +1,24 @@ +package cmd + +import ( + "fmt" + "os" + "train/pkg/installer" + + "github.com/spf13/cobra" +) + +var removeCmd = &cobra.Command{ + Use: "remove [package]", + Short: "Удаляет указанный пакет", + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + pkgName := args[0] + fmt.Printf("Удаление пакета: %s\n", pkgName) + if err := installer.RemovePackage(pkgName); err != nil { + fmt.Printf("Ошибка удаления: %v\n", err) + os.Exit(1) + } + fmt.Printf("Пакет %s успешно удалён.\n", pkgName) + }, +} diff --git a/cmd/repo.go b/cmd/repo.go new file mode 100644 index 0000000..10ece52 --- /dev/null +++ b/cmd/repo.go @@ -0,0 +1,64 @@ +package cmd + +import ( + "fmt" + "os" + "train/pkg/config" + + "github.com/spf13/cobra" +) + +var repoCmd = &cobra.Command{ + Use: "repo", + Short: "Управление репозиториями пакетов", +} + +var repoAddCmd = &cobra.Command{ + Use: "add [url]", + Short: "Добавляет новый репозиторий", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + url := args[0] + if err := config.AddRepository(url); err != nil { + fmt.Printf("Ошибка: %v\n", err) + os.Exit(1) + } + fmt.Printf("Репозиторий %s успешно добавлен.\n", url) + }, +} + +var repoRemoveCmd = &cobra.Command{ + Use: "remove [url]", + Short: "Удаляет репозиторий", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + url := args[0] + if err := config.RemoveRepository(url); err != nil { + fmt.Printf("Ошибка: %v\n", err) + os.Exit(1) + } + fmt.Printf("Репозиторий %s успешно удалён.\n", url) + }, +} + +var repoListCmd = &cobra.Command{ + Use: "list", + Short: "Список добавленных репозиториев", + Run: func(cmd *cobra.Command, args []string) { + cfg, err := config.LoadConfig() + if err != nil { + fmt.Printf("Ошибка загрузки конфигурации: %v\n", err) + os.Exit(1) + } + fmt.Println("Добавленные репозитории:") + for _, r := range cfg.Repositories { + fmt.Println(" -", r) + } + }, +} + +func init() { + repoCmd.AddCommand(repoAddCmd) + repoCmd.AddCommand(repoRemoveCmd) + repoCmd.AddCommand(repoListCmd) +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..6a19665 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,29 @@ +package cmd + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" +) + +var rootCmd = &cobra.Command{ + Use: "train", + Short: "Train - легковесный пакетный менеджер", +} + +func Execute() { + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +func init() { + rootCmd.AddCommand(installCmd) + rootCmd.AddCommand(removeCmd) + rootCmd.AddCommand(updateCmd) + rootCmd.AddCommand(listCmd) + rootCmd.AddCommand(infoCmd) + rootCmd.AddCommand(repoCmd) +} diff --git a/cmd/update.go b/cmd/update.go new file mode 100644 index 0000000..e11d5e9 --- /dev/null +++ b/cmd/update.go @@ -0,0 +1,24 @@ +package cmd + +import ( + "fmt" + "os" + "train/pkg/installer" + + "github.com/spf13/cobra" +) + +var updateCmd = &cobra.Command{ + Use: "update [package]", + Short: "Обновляет указанный пакет (удаление + установка)", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + pkgName := args[0] + fmt.Printf("Обновление пакета: %s\n", pkgName) + if err := installer.UpdatePackage(pkgName); err != nil { + fmt.Printf("Ошибка обновления: %v\n", err) + os.Exit(1) + } + fmt.Printf("Пакет %s успешно обновлён.\n", pkgName) + }, +} diff --git a/config.json b/config.json new file mode 100644 index 0000000..5ca4e6e --- /dev/null +++ b/config.json @@ -0,0 +1,6 @@ +{ + "repositories": [ + "http://xn--80abmlgju2eo3byb.xn--p1ai/sklad/" + ], + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2e60e31 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module train + +go 1.18 + +require github.com/spf13/cobra v1.7.0 // или актуальная версия + +require ( + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f3366a9 --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..6ec3929 --- /dev/null +++ b/main.go @@ -0,0 +1,9 @@ +package main + +import ( + "train/cmd" +) + +func main() { + cmd.Execute() +} diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 0000000..785021f --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,77 @@ +package config + +import ( + "encoding/json" + "errors" + "io/ioutil" + "os" +) + +type Config struct { + Repositories []string `json:"repositories"` +} + +var configFile = "./config.json" + +func LoadConfig() (*Config, error) { + if _, err := os.Stat(configFile); os.IsNotExist(err) { + cfg := &Config{Repositories: []string{}} + if err := SaveConfig(cfg); err != nil { + return nil, err + } + return cfg, nil + } + + data, err := ioutil.ReadFile(configFile) + if err != nil { + return nil, err + } + var cfg Config + if err := json.Unmarshal(data, &cfg); err != nil { + return nil, err + } + return &cfg, nil +} + +func SaveConfig(cfg *Config) error { + data, err := json.MarshalIndent(cfg, "", " ") + if err != nil { + return err + } + return ioutil.WriteFile(configFile, data, 0644) +} + +func AddRepository(repo string) error { + cfg, err := LoadConfig() + if err != nil { + return err + } + for _, r := range cfg.Repositories { + if r == repo { + return errors.New("репозиторий уже добавлен") + } + } + cfg.Repositories = append(cfg.Repositories, repo) + return SaveConfig(cfg) +} + +func RemoveRepository(repo string) error { + cfg, err := LoadConfig() + if err != nil { + return err + } + newRepos := []string{} + found := false + for _, r := range cfg.Repositories { + if r == repo { + found = true + continue + } + newRepos = append(newRepos, r) + } + if !found { + return errors.New("репозиторий не найден") + } + cfg.Repositories = newRepos + return SaveConfig(cfg) +} diff --git a/pkg/installer/installer.go b/pkg/installer/installer.go new file mode 100644 index 0000000..4d27857 --- /dev/null +++ b/pkg/installer/installer.go @@ -0,0 +1,254 @@ +package installer + +import ( + "archive/tar" + "compress/gzip" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + + "train/pkg/manifest" +) + +var ( + binInstallDir = "./installed_bins" // for binary + buildInstallDir = "./installed_packages" // for build" + installDir = buildInstallDir // for func +) + +func computeChecksum(filePath string) (string, error) { + data, err := ioutil.ReadFile(filePath) + if err != nil { + return "", err + } + hash := sha256.Sum256(data) + return hex.EncodeToString(hash[:]), nil +} + +func downloadFile(url string) (string, error) { + resp, err := http.Get(url) + if err != nil { + return "", err + } + defer resp.Body.Close() + + tmpFile, err := ioutil.TempFile("", "package_") + if err != nil { + return "", err + } + _, err = io.Copy(tmpFile, resp.Body) + if err != nil { + return "", err + } + tmpFile.Close() + return tmpFile.Name(), nil +} + +func unpackPackage(filePath, packageName string) error { + destDir := filepath.Join(buildInstallDir, packageName) + if err := os.MkdirAll(destDir, os.ModePerm); err != nil { + return err + } + + f, err := os.Open(filePath) + if err != nil { + return err + } + defer f.Close() + + gzr, err := gzip.NewReader(f) + if err != nil { + return err + } + defer gzr.Close() + + tr := tar.NewReader(gzr) + + for { + header, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return err + } + + target := filepath.Join(destDir, header.Name) + + switch header.Typeflag { + case tar.TypeDir: + if err := os.MkdirAll(target, os.FileMode(header.Mode)); err != nil { + return err + } + case tar.TypeReg: + outFile, err := os.Create(target) + if err != nil { + return err + } + if _, err := io.Copy(outFile, tr); err != nil { + outFile.Close() + return err + } + outFile.Close() + default: + + } + } + return nil +} + +func executeBuildCommands(commands []string, packageName string) error { + packagePath := filepath.Join(buildInstallDir, packageName) + for _, commandStr := range commands { + cmd := exec.Command("sh", "-c", commandStr) + cmd.Dir = packagePath + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("команда '%s' завершилась с ошибкой: %s", commandStr, string(output)) + } + } + return nil +} + +func saveInstalledPackage(name string, m *manifest.Manifest) error { + packagePath := filepath.Join(buildInstallDir, name) + if err := os.MkdirAll(packagePath, os.ModePerm); err != nil { + return err + } + jsonData, err := json.MarshalIndent(m, "", " ") + if err != nil { + return err + } + return ioutil.WriteFile(filepath.Join(packagePath, "manifest.json"), jsonData, 0644) +} + +func removeInstalledPackageRecord(name string) { +} + +func InstallPackage(name string) error { + m, err := manifest.FetchManifest(name) + if err != nil { + return fmt.Errorf("не удалось получить манифест: %w", err) + } + + if strings.ToLower(m.Mode) == "bin" { + platformKey := runtime.GOOS + "_" + runtime.GOARCH + binaryURL, ok := m.Binaries[platformKey] + if !ok { + return fmt.Errorf("нет бинарного файла для платформы %s", platformKey) + } + packageFile, err := downloadFile(binaryURL) + if err != nil { + return fmt.Errorf("не удалось скачать бинарник: %w", err) + } + defer os.Remove(packageFile) + + checksum, err := computeChecksum(packageFile) + if err != nil { + return fmt.Errorf("не удалось вычислить контрольную сумму: %w", err) + } + if checksum != m.Checksum { + return fmt.Errorf("контрольная сумма не совпадает, ожидалось %s, получено %s", m.Checksum, checksum) + } + + if err := os.MkdirAll(binInstallDir, os.ModePerm); err != nil { + return err + } + destPath := filepath.Join(binInstallDir, m.Name) + data, err := ioutil.ReadFile(packageFile) + if err != nil { + return err + } + if err := ioutil.WriteFile(destPath, data, 0755); err != nil { + return err + } + fmt.Printf("Бинарный пакет %s установлен в %s\n", m.Name, destPath) + return nil + } else if strings.ToLower(m.Mode) == "build" { + packageFile, err := downloadFile(m.Source) + if err != nil { + return fmt.Errorf("не удалось скачать пакет: %w", err) + } + defer os.Remove(packageFile) + + checksum, err := computeChecksum(packageFile) + if err != nil { + return fmt.Errorf("не удалось вычислить контрольную сумму: %w", err) + } + if checksum != m.Checksum { + return fmt.Errorf("контрольная сумма не совпадает, ожидалось %s, получено %s", m.Checksum, checksum) + } + + if err := unpackPackage(packageFile, name); err != nil { + return fmt.Errorf("не удалось распаковать пакет: %w", err) + } + + if len(m.Build) > 0 { + if err := executeBuildCommands(m.Build, name); err != nil { + return fmt.Errorf("ошибка выполнения команд сборки: %w", err) + } + } + + if err := saveInstalledPackage(name, m); err != nil { + return fmt.Errorf("не удалось сохранить информацию об установленном пакете: %w", err) + } + return nil + } + + return errors.New("неизвестный режим пакета") +} + +func RemovePackage(name string) error { + packagePath := filepath.Join(buildInstallDir, name) + if _, err := os.Stat(packagePath); os.IsNotExist(err) { + return errors.New("пакет не найден") + } + if err := os.RemoveAll(packagePath); err != nil { + return fmt.Errorf("не удалось удалить пакет: %w", err) + } + removeInstalledPackageRecord(name) + return nil +} + +func UpdatePackage(name string) error { + if err := RemovePackage(name); err != nil { + return fmt.Errorf("не удалось удалить пакет для обновления: %w", err) + } + if err := InstallPackage(name); err != nil { + return fmt.Errorf("не удалось установить пакет: %w", err) + } + return nil +} + +func ListInstalledPackages() ([]string, error) { + var pkgs []string + entries, err := ioutil.ReadDir(buildInstallDir) + if err != nil { + return nil, err + } + for _, entry := range entries { + if entry.IsDir() { + pkgs = append(pkgs, entry.Name()) + } + } + return pkgs, nil +} + +func GetPackageInfo(name string) (string, error) { + infoFile := filepath.Join(buildInstallDir, name, "manifest.json") + data, err := ioutil.ReadFile(infoFile) + if err != nil { + return "", err + } + return string(data), nil +} diff --git a/pkg/manifest/manifest.go b/pkg/manifest/manifest.go new file mode 100644 index 0000000..cf76198 --- /dev/null +++ b/pkg/manifest/manifest.go @@ -0,0 +1,58 @@ +package manifest + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "train/pkg/config" +) + +type Manifest struct { + Name string `json:"name"` + Version string `json:"version"` + Mode string `json:"mode"` + Source string `json:"source"` + Checksum string `json:"checksum"` + Dependencies []string `json:"dependencies"` + Build []string `json:"build"` + Binaries map[string]string `json:"binaries"` + runtime.GOOS+"_"+runtime.GOARCH +} + +func FetchManifest(name string) (*Manifest, error) { + cfg, err := config.LoadConfig() + if err != nil { + return nil, err + } + var lastErr error + for _, repo := range cfg.Repositories { + url := fmt.Sprintf("%s/%s/manifest.json", repo, name) + resp, err := http.Get(url) + if err != nil { + lastErr = err + continue + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + lastErr = errors.New("пакет не найден в репозитории " + repo) + continue + } + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + lastErr = err + continue + } + var m Manifest + if err := json.Unmarshal(data, &m); err != nil { + lastErr = err + continue + } + return &m, nil + } + if lastErr == nil { + lastErr = errors.New("не найден ни один репозиторий") + } + return nil, lastErr +}