Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Rust backend #77

Merged
merged 1 commit into from
Sep 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
github.com/natefinch/atomic v0.0.0-20150920032501-a62ce929ffcc
github.com/rakyll/statik v0.1.6
github.com/spf13/cobra v0.0.5
github.com/stretchr/testify v1.7.0 // indirect
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550
golang.org/x/tools v0.0.0-20200605181038-cef9fc3bc8f0 // indirect
gopkg.in/yaml.v2 v2.2.2
Expand Down
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
Expand All @@ -27,6 +29,7 @@ github.com/natefinch/atomic v0.0.0-20150920032501-a62ce929ffcc h1:7xGrl4tTpBQu5Z
github.com/natefinch/atomic v0.0.0-20150920032501-a62ce929ffcc/go.mod h1:1rLVY/DWf3U6vSZgH16S7pymfrhK2lcUlXjgGglw/lY=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rakyll/statik v0.1.6 h1:uICcfUXpgqtw2VopbIncslhAmE5hwc4g20TEyEENBNs=
github.com/rakyll/statik v0.1.6/go.mod h1:OEi9wJV/fMUAGx1eNjq75DKDsJVuEv1U0oYdX6GX8Zs=
Expand All @@ -43,7 +46,10 @@ github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.3.2 h1:VUFqw5KcqRf7i70GOzW7N+Q7+gxVBkSSqiXB12+JQ4M=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
Expand Down Expand Up @@ -76,3 +82,5 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
2 changes: 2 additions & 0 deletions internal/backends/backends.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/replit/upm/internal/backends/python"
"github.com/replit/upm/internal/backends/rlang"
"github.com/replit/upm/internal/backends/ruby"
"github.com/replit/upm/internal/backends/rust"
"github.com/replit/upm/internal/util"
)

Expand All @@ -33,6 +34,7 @@ var languageBackends = []api.LanguageBackend{
java.JavaBackend,
rlang.RlangBackend,
dotnet.DotNetBackend,
rust.RustBackend,
}

// matchesLanguage checks if a language backend matches a value for
Expand Down
4 changes: 3 additions & 1 deletion internal/backends/backends_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ func TestGetBackends(t *testing.T) {
"Setup.fs": "dotnet",
"project.fsproj": "dotnet",
"pom.xml": "java-maven",
"package.json": "nodejs-npm"}
"package.json": "nodejs-npm",
"Cargo.toml": "rust",
}

dir, err := ioutil.TempDir("", "TestGetBackends")
if err != nil {
Expand Down
259 changes: 259 additions & 0 deletions internal/backends/rust/rust.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
// Package rust provides a backend for Rust using Cargo.
package rust

import (
"encoding/json"
"io/ioutil"
"net/http"
"net/url"

"github.com/BurntSushi/toml"
"github.com/replit/upm/internal/api"
"github.com/replit/upm/internal/util"
)

type cargoToml struct {
Dependencies map[string]interface{} `toml:"dependencies"`
}

type cargoLock struct {
Packages []cargoPackage `toml:"package"`
}

type cargoPackage struct {
Name string `toml:"name"`
Version string `toml:"version"`
}

type crateSearchResults struct {
Crates []crate `json:"crates"`
}

type crateInfoResult struct {
Crate crate `json:"crate"`
Versions []version `json:"versions"`
}

type crate struct {
Name string `json:"name"`
Description string `json:"description"`
Homepage string `json:"homepage"`
Documentation string `json:"documentation"`
Repository string `json:"repository"`
NewestVersion string `json:"newest_version"`
Versions []int `json:"versions"`
}

type version struct {
Num string `json:"num"`
PublishedBy user `json:"published_by"`
License string `json:"license"`
}

type user struct {
Name string `json:"name"`
}

func (c *crateInfoResult) toPkgInfo() api.PkgInfo {
var author string
var license string

for _, version := range c.Versions {
if version.Num == c.Crate.NewestVersion {
author = version.PublishedBy.Name
license = version.License
break
}
}

return api.PkgInfo{
Name: c.Crate.Name,
Description: c.Crate.Description,
Version: c.Crate.NewestVersion,
HomepageURL: c.Crate.Homepage,
DocumentationURL: c.Crate.Documentation,
SourceCodeURL: c.Crate.Repository,
Author: author,
License: license,
}
}

func search(query string) []api.PkgInfo {
endpoint := "https://crates.io/api/v1/crates"
path := "?q=" + url.QueryEscape(query)

resp, err := http.Get(endpoint + path)
if err != nil {
util.Die("crates.io: %s", err)
}
defer resp.Body.Close()

body, err := ioutil.ReadAll(resp.Body)
if err != nil {
util.Die("crates.io: %s", err)
}

var crateResults crateSearchResults
if err := json.Unmarshal(body, &crateResults); err != nil {
util.Die("crates.io: %s", err)
}

var pkgs []api.PkgInfo
for _, crate := range crateResults.Crates {
crateInfo := crateInfoResult{
Crate: crate,
}
pkgs = append(pkgs, crateInfo.toPkgInfo())
}

return pkgs
}

func info(name api.PkgName) api.PkgInfo {
endpoint := "https://crates.io/api/v1/crates"
path := "/" + url.PathEscape(string(name))

resp, err := http.Get(endpoint + path)
if err != nil {
util.Die("crates.io: %s", err)
}
defer resp.Body.Close()

switch resp.StatusCode {
case 200:
break
case 404:
return api.PkgInfo{}
default:
util.Die("crates.io: HTTP status %d", resp.StatusCode)
}

body, err := ioutil.ReadAll(resp.Body)
var crateInfo crateInfoResult
if err := json.Unmarshal(body, &crateInfo); err != nil {
util.Die("crates.io: %s", err)
}

return crateInfo.toPkgInfo()
}

func listSpecfile() map[api.PkgName]api.PkgSpec {
contents, err := ioutil.ReadFile("Cargo.toml")
if err != nil {
util.Die("Cargo.toml: %s", err)
}

return listSpecfileWithContents(contents)
}

func listSpecfileWithContents(contents []byte) map[api.PkgName]api.PkgSpec {
var specfile cargoToml
err := toml.Unmarshal(contents, &specfile)
if err != nil {
util.Die("Cargo.toml: %s", err)
}

packages := make(map[api.PkgName]api.PkgSpec)
for name, dependency := range specfile.Dependencies {
var spec api.PkgSpec
switch value := dependency.(type) {
case string:
spec = api.PkgSpec(value)

case map[string]interface{}:
found := false
for _, key := range []string{"version", "git", "path"} {
specStr, ok := value[key].(string)
if !ok {
continue
}

spec = api.PkgSpec(specStr)
found = true
break
}

if !found {
util.Die("Cargo.toml: could not determine spec for dependecy %q", name)
}

default:
util.Die("Cargo.toml: unexpected dependency format %q", name)
}

packages[api.PkgName(name)] = spec

}

return packages
}

func listLockfile() map[api.PkgName]api.PkgVersion {
contents, err := ioutil.ReadFile("Cargo.lock")
if err != nil {
util.Die("Cargo.lock: %s", err)
}

return listLockfileWithContents(contents)
}

func listLockfileWithContents(contents []byte) map[api.PkgName]api.PkgVersion {
var lockfile cargoLock
err := toml.Unmarshal(contents, &lockfile)
if err != nil {
util.Die("Cargo.lock: %s", err)
}

packages := make(map[api.PkgName]api.PkgVersion)
for _, pkg := range lockfile.Packages {
packages[api.PkgName(pkg.Name)] = api.PkgVersion(pkg.Version)
}

return packages
}

// RustBackend is a UPM backend for Rust that uses Cargo.
var RustBackend = api.LanguageBackend{
Name: "rust",
Specfile: "Cargo.toml",
Lockfile: "Cargo.lock",
FilenamePatterns: []string{"*.rs"},
GetPackageDir: func() string {
return "target"
},
Search: search,
Info: info,
Add: func(pkgs map[api.PkgName]api.PkgSpec, projectName string) {
if !util.Exists("Cargo.toml") {
util.RunCmd([]string{"cargo", "init", "."})
}
cmd := []string{"cargo", "add"}
for name, spec := range pkgs {
arg := string(name)
if spec != "" {
arg += "@" + string(spec)
}
cmd = append(cmd, arg)
}
util.RunCmd(cmd)
},
Remove: func(pkgs map[api.PkgName]bool) {
cmd := []string{"cargo", "rm"}
for name := range pkgs {
cmd = append(cmd, string(name))
}
util.RunCmd(cmd)
},
Lock: func() {
// Lock file is updated at build time
},
Install: func() {
// Dependencies are installed at build time
},
ListSpecfile: listSpecfile,
ListLockfile: listLockfile,
cbrewster marked this conversation as resolved.
Show resolved Hide resolved
Guess: func() (map[api.PkgName]bool, bool) {
util.NotImplemented()
return nil, false
},
}
Loading