Some checks failed
kotlin / test (push) Has been skipped
python / test (push) Has been skipped
typescript / test (push) Has been skipped
go / build darwin/amd64 (push) Has been cancelled
go / build linux/amd64 (push) Has been cancelled
go / build windows/amd64 (push) Has been cancelled
go / build darwin/arm64 (push) Has been cancelled
go / build linux/arm64 (push) Has been cancelled
go / build windows/arm64 (push) Has been cancelled
go / test (push) Has been cancelled
go / vuln_check (push) Has been cancelled
222 lines
7.1 KiB
Go
222 lines
7.1 KiB
Go
package clickhouse
|
|
|
|
import (
|
|
"archive/tar"
|
|
"compress/gzip"
|
|
"context"
|
|
"crypto/sha512"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// DefaultVersion is the ClickHouse release the dialect is generated from.
|
|
// Bumping it is a deliberate change: the query tree format and type
|
|
// inference can shift between releases, so regenerate and review the dialect
|
|
// after changing it, and add the new release's assets to the table below.
|
|
const DefaultVersion = "25.8.2.29"
|
|
|
|
// asset is one downloadable build of ClickHouse. Linux builds are published
|
|
// as clickhouse-common-static tarballs holding the binary at
|
|
// usr/bin/clickhouse; macOS builds are published as bare binaries.
|
|
type asset struct {
|
|
Version string
|
|
OS string
|
|
Arch string
|
|
Name string
|
|
SHA512 string
|
|
}
|
|
|
|
// assets lists every build Install knows how to fetch, with the SHA-512 of
|
|
// the download. A version that is not in this table cannot be installed:
|
|
// verifying the download is the point of the table.
|
|
//
|
|
// The tarball checksums are the ones in the .sha512 files ClickHouse
|
|
// publishes next to them. ClickHouse publishes no checksum for the macOS
|
|
// binaries, so those were computed from the downloads.
|
|
var assets = []asset{
|
|
{"25.8.2.29", "linux", "amd64", "clickhouse-common-static-25.8.2.29-amd64.tgz", "6ff0aa1ffac6e564970174422ecde0d645cdb96812247a6e544d39cad6d78a514265f90a2bc7b4bad49903cea96eddd16a415a45b2aeaf9164461be76331bdee"},
|
|
{"25.8.2.29", "linux", "arm64", "clickhouse-common-static-25.8.2.29-arm64.tgz", "68204ca4d4e472790f808ee376251fae82e58066a31f35a40d15d442ce5988d697f18a1208d28b8bb8e2dfad4b20b7fcb5107e2178472abcd97251b8de7f058e"},
|
|
{"25.8.2.29", "darwin", "amd64", "clickhouse-macos", "2805805ad2506e37a3e71b4ae9e797bdc010a9368dc28e99bcaaa2c70a72cfdd031c0fce8fc304248fad73211d28d645f75c6432cfae1e1e54d72d04e8626cd4"},
|
|
{"25.8.2.29", "darwin", "arm64", "clickhouse-macos-aarch64", "4c9237e85c8d4e1aced2b339b32e086b4f23fa14f99754b056a7e33dda88c4fb5d52e09e29d20181ec5b580caadb00f36613802e71755ff34927535eb8babf79"},
|
|
}
|
|
|
|
// releaseTag returns the GitHub release tag for a version. ClickHouse tags
|
|
// its March and August releases as LTS and everything else as stable.
|
|
func releaseTag(version string) (string, error) {
|
|
parts := strings.Split(version, ".")
|
|
if len(parts) != 4 {
|
|
return "", fmt.Errorf("invalid ClickHouse version %q: want MAJOR.MINOR.PATCH.BUILD", version)
|
|
}
|
|
minor, err := strconv.Atoi(parts[1])
|
|
if err != nil {
|
|
return "", fmt.Errorf("invalid ClickHouse version %q: %w", version, err)
|
|
}
|
|
suffix := "-stable"
|
|
if minor == 3 || minor == 8 {
|
|
suffix = "-lts"
|
|
}
|
|
return "v" + version + suffix, nil
|
|
}
|
|
|
|
// releaseAsset finds the build for a platform in the table.
|
|
func releaseAsset(version, goos, goarch string) (asset, error) {
|
|
for _, a := range assets {
|
|
if a.Version == version && a.OS == goos && a.Arch == goarch {
|
|
return a, nil
|
|
}
|
|
}
|
|
for _, a := range assets {
|
|
if a.Version == version {
|
|
return asset{}, fmt.Errorf("no ClickHouse %s build is listed for %s/%s", version, goos, goarch)
|
|
}
|
|
}
|
|
return asset{}, fmt.Errorf("ClickHouse %s is not in the asset table; add its downloads and checksums to install.go", version)
|
|
}
|
|
|
|
// url is the asset's download address on GitHub.
|
|
func (a asset) url() (string, error) {
|
|
tag, err := releaseTag(a.Version)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return "https://github.com/ClickHouse/ClickHouse/releases/download/" + tag + "/" + a.Name, nil
|
|
}
|
|
|
|
// tarball reports whether the download is an archive rather than the binary.
|
|
func (a asset) tarball() bool {
|
|
return strings.HasSuffix(a.Name, ".tgz")
|
|
}
|
|
|
|
// cachedBinary is where Install puts the binary for a version.
|
|
func cachedBinary(version string) (string, error) {
|
|
dir, err := os.UserCacheDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return filepath.Join(dir, "sqlc-clickhouse", version, "clickhouse"), nil
|
|
}
|
|
|
|
// Locate finds a clickhouse binary: the CLICKHOUSE environment variable wins,
|
|
// then the cached copy of DefaultVersion.
|
|
func Locate() (string, error) {
|
|
if path := os.Getenv("CLICKHOUSE"); path != "" {
|
|
return path, nil
|
|
}
|
|
path, err := cachedBinary(DefaultVersion)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if _, err := os.Stat(path); err != nil {
|
|
return "", fmt.Errorf("clickhouse %s is not installed: run `go run ./cmd/goldeneye install clickhouse` in internal/goldeneye, or set CLICKHOUSE to a clickhouse binary", DefaultVersion)
|
|
}
|
|
return path, nil
|
|
}
|
|
|
|
// Install downloads the clickhouse binary for a version into the cache and
|
|
// returns its path. It is a no-op when the version is already cached. The
|
|
// download is checked against the table's SHA-512 before it is installed.
|
|
func Install(ctx context.Context, version, goos, goarch string, progress io.Writer) (string, error) {
|
|
dest, err := cachedBinary(version)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if _, err := os.Stat(dest); err == nil {
|
|
return dest, nil
|
|
}
|
|
a, err := releaseAsset(version, goos, goarch)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
url, err := a.url()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
fmt.Fprintf(progress, "downloading %s\n", url)
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
return "", fmt.Errorf("downloading %s: %s", url, resp.Status)
|
|
}
|
|
|
|
// Write next to the destination and rename so a partial or corrupt
|
|
// download never masquerades as an installed binary.
|
|
tmp, err := os.CreateTemp(filepath.Dir(dest), "clickhouse-*.partial")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer os.Remove(tmp.Name())
|
|
|
|
// Hash every byte that comes off the wire, including the parts of a
|
|
// tarball after the binary, which extraction would otherwise not read.
|
|
sum := sha512.New()
|
|
body := io.TeeReader(resp.Body, sum)
|
|
var src io.Reader = body
|
|
if a.tarball() {
|
|
src, err = binaryInTarball(body)
|
|
if err != nil {
|
|
return "", fmt.Errorf("downloading %s: %w", url, err)
|
|
}
|
|
}
|
|
if _, err := io.Copy(tmp, src); err != nil {
|
|
tmp.Close()
|
|
return "", err
|
|
}
|
|
if _, err := io.Copy(io.Discard, body); err != nil {
|
|
tmp.Close()
|
|
return "", err
|
|
}
|
|
if err := tmp.Close(); err != nil {
|
|
return "", err
|
|
}
|
|
if got := hex.EncodeToString(sum.Sum(nil)); got != a.SHA512 {
|
|
return "", fmt.Errorf("downloading %s: SHA-512 mismatch: got %s, want %s", url, got, a.SHA512)
|
|
}
|
|
if err := os.Chmod(tmp.Name(), 0o755); err != nil {
|
|
return "", err
|
|
}
|
|
if err := os.Rename(tmp.Name(), dest); err != nil {
|
|
return "", err
|
|
}
|
|
return dest, nil
|
|
}
|
|
|
|
// binaryInTarball positions a reader at the clickhouse binary inside a
|
|
// clickhouse-common-static tarball.
|
|
func binaryInTarball(r io.Reader) (io.Reader, error) {
|
|
gz, err := gzip.NewReader(r)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tr := tar.NewReader(gz)
|
|
for {
|
|
hdr, err := tr.Next()
|
|
if errors.Is(err, io.EOF) {
|
|
return nil, errors.New("tarball does not contain usr/bin/clickhouse")
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if hdr.Typeflag == tar.TypeReg && strings.HasSuffix(hdr.Name, "/usr/bin/clickhouse") {
|
|
return tr, nil
|
|
}
|
|
}
|
|
}
|