diff --git a/Makefile b/Makefile index de6d080e8c282..2cfbc53ced77b 100644 --- a/Makefile +++ b/Makefile @@ -88,7 +88,8 @@ errdoc:tools/bin/errdoc-gen .PHONY: lint lint:tools/bin/revive @echo "linting" - @tools/bin/revive -formatter friendly -config tools/check/revive.toml $(FILES_TIDB_TESTS) + @tools/bin/revive -formatter friendly -config tools/check/revive.toml \ + -exclude pkg/util/hack/... -exclude ./pkg/util/hack/... $(FILES_TIDB_TESTS) @tools/bin/revive -formatter friendly -config tools/check/revive.toml ./lightning/... go run tools/dashboard-linter/main.go pkg/metrics/grafana/overview.json go run tools/dashboard-linter/main.go pkg/metrics/grafana/performance_overview.json diff --git a/Makefile.common b/Makefile.common index d69ac8d557813..b7140e7096fe4 100644 --- a/Makefile.common +++ b/Makefile.common @@ -66,15 +66,15 @@ LINUX := "Linux" MAC := "Darwin" PACKAGE_LIST := go list ./... -PACKAGE_LIST_TIDB_TESTS := go list ./... | grep -vE "github.com\/pingcap\/tidb\/br|github.com\/pingcap\/tidb\/cmd|github.com\/pingcap\/tidb\/dumpling" +PACKAGE_LIST_TIDB_TESTS := go list ./... | grep -vE "github.com/pingcap/tidb/br|github.com/pingcap/tidb/cmd|github.com/pingcap/tidb/dumpling" PACKAGES ?= $$($(PACKAGE_LIST)) PACKAGES_TIDB_TESTS ?= $$($(PACKAGE_LIST_TIDB_TESTS)) PACKAGE_DIRECTORIES := $(PACKAGE_LIST) | sed 's|github.com/pingcap/$(PROJECT)/||' PACKAGE_DIRECTORIES_TIDB_TESTS := $(PACKAGE_LIST_TIDB_TESTS) | sed 's|github.com/pingcap/$(PROJECT)/||' FILES := $$(find $$($(PACKAGE_DIRECTORIES)) -name "*.go") -FILES_TIDB_TESTS := $$(find $$($(PACKAGE_DIRECTORIES_TIDB_TESTS)) -name "*.go") +FILES_TIDB_TESTS := $$(find $$($(PACKAGE_DIRECTORIES_TIDB_TESTS)) -name "*.go" -not -path "pkg/util/hack/*") -UNCONVERT_PACKAGES_LIST := go list ./...| grep -vE "lightning\/checkpoints|lightning\/manual|lightning\/common|tidb-binlog\/proto\/go-binlog" +UNCONVERT_PACKAGES_LIST := go list ./...| grep -vE "lightning/checkpoints|lightning/manual|lightning/common|tidb-binlog/proto/go-binlog" UNCONVERT_PACKAGES := $$($(UNCONVERT_PACKAGES_LIST)) FAILPOINT_ENABLE := find $$PWD/ -mindepth 1 -type d | grep -vE "(\.git|\.idea|tools)" | xargs tools/bin/failpoint-ctl enable @@ -114,7 +114,7 @@ ifeq ("$(WITH_CHECK)", "1") endif BR_PKG := github.com/pingcap/tidb/br -BR_PACKAGES := go list ./...| grep "github.com\/pingcap\/tidb\/br" +BR_PACKAGES := go list ./...| grep "github.com/pingcap/tidb/br" BR_PACKAGE_DIRECTORIES := $(BR_PACKAGES) | sed 's|github.com/pingcap/$(PROJECT)/||' LIGHTNING_BIN := bin/tidb-lightning LIGHTNING_CTL_BIN := bin/tidb-lightning-ctl @@ -123,7 +123,7 @@ TEST_DIR := /tmp/backup_restore_test DUMPLING_PKG := github.com/pingcap/tidb/dumpling -DUMPLING_PACKAGES := go list ./... | grep 'github.com\/pingcap\/tidb\/dumpling' +DUMPLING_PACKAGES := go list ./... | grep 'github.com/pingcap/tidb/dumpling' DUMPLING_PACKAGE_DIRECTORIES := $(DUMPLING_PACKAGES) | sed 's|github.com/pingcap/$(PROJECT)/||' DUMPLING_BIN := bin/dumpling DUMPLING_CHECKER := awk '{ print } END { if (NR > 0) { exit 1 } }' diff --git a/pkg/util/hack/BUILD.bazel b/pkg/util/hack/BUILD.bazel index bbd6caefd3dd4..339fcc2c23972 100644 --- a/pkg/util/hack/BUILD.bazel +++ b/pkg/util/hack/BUILD.bazel @@ -5,6 +5,7 @@ go_library( srcs = [ "hack.go", "map_abi.go", + "map_abi_go126.go", ], importpath = "github.com/pingcap/tidb/pkg/util/hack", visibility = ["//visibility:public"], @@ -17,6 +18,8 @@ go_test( "hack_test.go", "main_test.go", "map_abi_test.go", + "map_abi_test_type_go125_test.go", + "map_abi_test_type_go126_test.go", ], embed = [":hack"], flaky = True, diff --git a/pkg/util/hack/map_abi.go b/pkg/util/hack/map_abi.go index 170ce960fd863..60950e8d42c7b 100644 --- a/pkg/util/hack/map_abi.go +++ b/pkg/util/hack/map_abi.go @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +//go:build go1.25 && !go1.26 + package hack import ( diff --git a/pkg/util/hack/map_abi_go126.go b/pkg/util/hack/map_abi_go126.go new file mode 100644 index 0000000000000..bfd52e79d8f27 --- /dev/null +++ b/pkg/util/hack/map_abi_go126.go @@ -0,0 +1,426 @@ +// Copyright 2025 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build go1.26 && !go1.27 + +package hack + +import ( + "runtime" + "strings" + "unsafe" +) + +// Maximum size of a table before it is split at the directory level. +const maxTableCapacity = 1024 + +// Number of bits in the group.slot count. +const mapGroupSlotsBits = 3 + +// Number of slots in a group. +const mapGroupSlots = 1 << mapGroupSlotsBits // 8 + +// $GOROOT/src/internal/runtime/maps/table.go:`type table struct` +type mapTable struct { + // The number of filled slots (i.e. the number of elements in the table). + used uint16 + + // The total number of slots (always 2^N). Equal to + // `(groups.lengthMask+1)*abi.MapGroupSlots`. + capacity uint16 + + // The number of slots we can still fill without needing to rehash. + // + // We rehash when used + tombstones > loadFactor*capacity, including + // tombstones so the table doesn't overfill with tombstones. This field + // counts down remaining empty slots before the next rehash. + growthLeft uint16 + + // The number of bits used by directory lookups above this table. Note + // that this may be less then globalDepth, if the directory has grown + // but this table has not yet been split. + localDepth uint8 + + // Index of this table in the Map directory. This is the index of the + // _first_ location in the directory. The table may occur in multiple + // sequential indicies. + // + // index is -1 if the table is stale (no longer installed in the + // directory). + index int + + // groups is an array of slot groups. Each group holds abi.MapGroupSlots + // key/elem slots and their control bytes. A table has a fixed size + // groups array. The table is replaced (in rehash) when more space is + // required. + // + // TODO(prattmic): keys and elements are interleaved to maximize + // locality, but it comes at the expense of wasted space for some types + // (consider uint8 key, uint64 element). Consider placing all keys + // together in these cases to save space. + groups groupsReference +} + +// groupsReference is a wrapper type describing an array of groups stored at +// data. +type groupsReference struct { + // data points to an array of groups. See groupReference above for the + // definition of group. + data unsafe.Pointer // data *[length]typ.Group + + // lengthMask is the number of groups in data minus one (note that + // length must be a power of two). This allows computing i%length + // quickly using bitwise AND. + lengthMask uint64 +} + +// $GOROOT/src/internal/runtime/maps/map.go:`type Map struct` +type mapData struct { + // The number of filled slots (i.e. the number of elements in all + // tables). Excludes deleted slots. + // Must be first (known by the compiler, for len() builtin). + Used uint64 + + // seed is the hash seed, computed as a unique random number per map. + seed uintptr + + // The directory of tables. + // + // Normally dirPtr points to an array of table pointers + // + // dirPtr *[dirLen]*table + // + // The length (dirLen) of this array is `1 << globalDepth`. Multiple + // entries may point to the same table. See top-level comment for more + // details. + // + // Small map optimization: if the map always contained + // abi.MapGroupSlots or fewer entries, it fits entirely in a + // single group. In that case dirPtr points directly to a single group. + // + // dirPtr *group + // + // In this case, dirLen is 0. used counts the number of used slots in + // the group. Note that small maps never have deleted slots (as there + // is no probe sequence to maintain). + dirPtr unsafe.Pointer + dirLen int + + // The number of bits to use in table directory lookups. + globalDepth uint8 + + // The number of bits to shift out of the hash for directory lookups. + // On 64-bit systems, this is 64 - globalDepth. + globalShift uint8 + + // writing is a flag that is toggled (XOR 1) while the map is being + // written. Normally it is set to 1 when writing, but if there are + // multiple concurrent writers, then toggling increases the probability + // that both sides will detect the race. + writing uint8 + + // tombstonePossible is false if we know that no table in this map + // contains a tombstone. + tombstonePossible bool + + // clearSeq is a sequence counter of calls to Clear. It is used to + // detect map clears during iteration. + clearSeq uint64 +} + +func (m *mapData) directoryAt(i uintptr) *mapTable { + return *(**mapTable)(unsafe.Pointer(uintptr(m.dirPtr) + uintptr(sizeofPtr)*i)) +} + +// Size returns the accurate memory size of the map including all its tables. +func (m *mapData) Size(groupSize uint64) (sz uint64) { + sz += mapSize + sz += sizeofPtr * uint64(m.dirLen) + if m.dirLen == 0 { + sz += groupSize + return + } + + var lastTab *mapTable + for i := range m.dirLen { + t := m.directoryAt(uintptr(i)) + if t == lastTab { + continue + } + lastTab = t + sz += mapTableSize + sz += groupSize * (t.groups.lengthMask + 1) + } + return +} + +// Cap returns the total capacity of the map. +func (m *mapData) Cap() uint64 { + if m.dirLen == 0 { + return mapGroupSlots + } + var capacity uint64 + var lastTab *mapTable + for i := range m.dirLen { + t := m.directoryAt(uintptr(i)) + if t == lastTab { + continue + } + lastTab = t + capacity += uint64(t.capacity) + } + return capacity +} + +// Size returns the accurate memory size +func (m *SwissMapWrap) Size() uint64 { + return m.Data.Size(uint64(m.Type.GroupSize)) +} + +const ( + mapSize = uint64(unsafe.Sizeof(mapData{})) + mapTableSize = uint64(unsafe.Sizeof(mapTable{})) + sizeofPtr = uint64(unsafe.Sizeof(uintptr(0))) +) + +// TODO: use a more accurate size calculation if necessary +func approxSize(groupSize uint64, maxLen uint64) (size uint64) { + // 204 can fit the `split`/`rehash` behavior of different kinds of map tables. + const ratio = 204 + return groupSize * maxLen * ratio / 1000 +} + +type ctrlGroup uint64 + +type groupReference struct { + // data points to the group, which is described by typ.Group and has + // layout: + // + // type group struct { + // ctrls ctrlGroup + // slots [abi.MapGroupSlots]slot + // } + // + // type slot struct { + // key typ.Key + // elem typ.Elem + // } + data unsafe.Pointer // data *typ.Group +} + +func (g *groupsReference) group(typ *mapType, i uint64) groupReference { + // TODO(prattmic): Do something here about truncation on cast to + // uintptr on 32-bit systems? + offset := uintptr(i) * typ.GroupSize + + return groupReference{ + data: unsafe.Pointer(uintptr(g.data) + offset), + } +} + +// $GOROOT/src/internal/abi/type.go:`type Type struct` +type abiType struct { + Size_ uintptr + PtrBytes uintptr // number of (prefix) bytes in the type that can contain pointers + Hash uint32 // hash of type; avoids computation in hash tables + TFlag uint8 // extra type information flags + Align_ uint8 // alignment of variable with this type + FieldAlign_ uint8 // alignment of struct field with this type + Kind_ uint8 // enumeration for C + // function for comparing objects of this type + // (ptr to object A, ptr to object B) -> ==? + Equal func(unsafe.Pointer, unsafe.Pointer) bool + // GCData stores the GC type data for the garbage collector. + // Normally, GCData points to a bitmask that describes the + // ptr/nonptr fields of the type. The bitmask will have at + // least PtrBytes/ptrSize bits. + // If the TFlagGCMaskOnDemand bit is set, GCData is instead a + // **byte and the pointer to the bitmask is one dereference away. + // The runtime will build the bitmask if needed. + // (See runtime/type.go:getGCMask.) + // Note: multiple types may have the same value of GCData, + // including when TFlagGCMaskOnDemand is set. The types will, of course, + // have the same pointer layout (but not necessarily the same size). + GCData *byte + Str int32 // string form + PtrToThis int32 // type for pointer to this type, may be zero +} + +// $GOROOT/src/internal/abi/map.go:`type MapType struct` +type mapType struct { + abiType + Key *abiType + Elem *abiType + Group *abiType // internal type representing a slot group + // function for hashing keys (ptr to key, seed) -> hash + Hasher func(unsafe.Pointer, uintptr) uintptr + GroupSize uintptr // == Group.Size_ + SlotSize uintptr // size of key/elem slot + ElemOff uintptr // offset of elem in key/elem slot; aka key size; elem size: SlotSize - ElemOff; + Flags uint32 +} + +// SwissMapWrap is a wrapper of map to access its internal structure. +type SwissMapWrap struct { + Type *mapType + Data *mapData +} + +// ToSwissMap converts a map to SwissMapWrap. +func ToSwissMap[K comparable, V any](m map[K]V) (sm SwissMapWrap) { + ref := any(m) + sm = *(*SwissMapWrap)(unsafe.Pointer(&ref)) + return +} + +const ( + ctrlGroupsSize = unsafe.Sizeof(ctrlGroup(0)) + groupSlotsOffset = ctrlGroupsSize +) + +func (g *groupReference) cap(typ *mapType) uint64 { + _ = g + return groupCap(uint64(typ.GroupSize), uint64(typ.SlotSize)) +} + +func groupCap(groupSize, slotSize uint64) uint64 { + return (groupSize - uint64(groupSlotsOffset)) / slotSize +} + +// key returns a pointer to the key at index i. +func (g *groupReference) key(typ *mapType, i uintptr) unsafe.Pointer { + offset := groupSlotsOffset + i*typ.SlotSize + return unsafe.Pointer(uintptr(g.data) + offset) +} + +// elem returns a pointer to the element at index i. +func (g *groupReference) elem(typ *mapType, i uintptr) unsafe.Pointer { + offset := groupSlotsOffset + i*typ.SlotSize + typ.ElemOff + return unsafe.Pointer(uintptr(g.data) + offset) +} + +// MemAwareMap is a map with memory usage tracking. +type MemAwareMap[K comparable, V any] struct { + M map[K]V + groupSize uint64 + nextCheckpoint uint64 // every `maxTableCapacity` increase in Used + Bytes uint64 +} + +// MockSeedForTest sets the seed of the map internals inside MemAwareMap. +func (m *MemAwareMap[K, V]) MockSeedForTest(seed uint64) (oriSeed uint64) { + return m.unwrap().MockSeedForTest(seed) +} + +// MockSeedForTest sets the seed of the map internals. +func (m *mapData) MockSeedForTest(seed uint64) (oriSeed uint64) { + if m.Used != 0 { + panic("MockSeedForTest can only be called on empty map") + } + oriSeed = uint64(m.seed) + m.seed = uintptr(seed) + return +} + +// Count returns the number of elements in the map. +func (m *MemAwareMap[K, V]) Count() int { + return len(m.M) +} + +// Empty returns true if the map is empty. +func (m *MemAwareMap[K, V]) Empty() bool { + return len(m.M) == 0 +} + +// Exist returns true if the key exists in the map. +func (m *MemAwareMap[K, V]) Exist(val K) bool { + _, ok := m.M[val] + return ok +} + +func (m *MemAwareMap[K, V]) unwrap() *mapData { + return *(**mapData)(unsafe.Pointer(&m.M)) +} + +// Set sets the value for the key in the map and returns the memory delta. +func (m *MemAwareMap[K, V]) Set(key K, value V) (deltaBytes int64) { + sm := m.unwrap() + m.M[key] = value + if sm.Used >= m.nextCheckpoint { + newBytes := max(m.Bytes, approxSize(m.groupSize, sm.Used)) + deltaBytes = int64(newBytes) - int64(m.Bytes) + m.Bytes = newBytes + m.nextCheckpoint = min(sm.Used, maxTableCapacity) + sm.Used + } + return +} + +// SetExt sets the value for the key in the map and returns the memory delta and whether it's an insert. +func (m *MemAwareMap[K, V]) SetExt(key K, value V) (deltaBytes int64, insert bool) { + sm := m.unwrap() + oriUsed := sm.Used + deltaBytes = m.Set(key, value) + insert = oriUsed != sm.Used + return +} + +// Init initializes the MemAwareMap with the given map and returns the initial memory size. +// The input map should NOT be nil. +func (m *MemAwareMap[K, V]) Init(v map[K]V) int64 { + if v == nil { + panic("MemAwareMap.Init: input map should NOT be nil") + } + m.M = v + sm := m.unwrap() + + m.groupSize = uint64(ToSwissMap(m.M).Type.GroupSize) + m.Bytes = sm.Size(m.groupSize) + if sm.Used <= mapGroupSlots { + m.nextCheckpoint = mapGroupSlots * 2 + } else { + m.nextCheckpoint = min(sm.Used, maxTableCapacity) + sm.Used + } + return int64(m.Bytes) +} + +// NewMemAwareMap creates a new MemAwareMap with the given initial capacity. +func NewMemAwareMap[K comparable, V any](capacity int) *MemAwareMap[K, V] { + m := new(MemAwareMap[K, V]) + m.Init(make(map[K]V, capacity)) + return m +} + +// RealBytes returns the real memory size of the map. +// Compute the real size is expensive, so do not call it frequently. +// Make sure the `seed` is same when testing the memory size. +func (m *MemAwareMap[K, V]) RealBytes() uint64 { + return m.unwrap().Size(m.groupSize) +} + +func checkMapABI() { + if !strings.Contains(runtime.Version(), `go1.26`) { + panic("The hack package only supports go1.26, please confirm the correctness of the ABI before upgrading") + } +} + +// Get the value of the key. +func (m *MemAwareMap[K, V]) Get(k K) (v V, ok bool) { + v, ok = m.M[k] + return +} + +// Len returns the number of elements in the map. +func (m *MemAwareMap[K, V]) Len() int { + return len(m.M) +} diff --git a/pkg/util/hack/map_abi_test.go b/pkg/util/hack/map_abi_test.go index 51692c34f48e1..ffc51cae696d4 100644 --- a/pkg/util/hack/map_abi_test.go +++ b/pkg/util/hack/map_abi_test.go @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +//go:build (go1.25 && !go1.26) || (go1.26 && !go1.27) + package hack import ( @@ -73,7 +75,7 @@ func TestSwissTable(t *testing.T) { require.Equal(t, N+1, len(mp)) require.Equal(t, uint64(N+1), sm.Data.Used) found := false - var lastTab *swissMapTable + var lastTab *testMapTable for i := range sm.Data.dirLen { table := sm.Data.directoryAt(uintptr(i)) @@ -84,7 +86,7 @@ func TestSwissTable(t *testing.T) { ref := table.groups.group(sm.Type, i) require.True(t, (sm.Type.GroupSize-groupSlotsOffset)%sm.Type.SlotSize == 0) capacity := ref.cap(sm.Type) - require.True(t, capacity == swissMapGroupSlots) + require.True(t, capacity == testMapGroupSlots) for j := range capacity { k, v := *(*uint64)(ref.key(sm.Type, uintptr(j))), *(*uint64)(ref.elem(sm.Type, uintptr(j))) if k == 1234 && v == 5678 { diff --git a/pkg/util/hack/map_abi_test_type_go125_test.go b/pkg/util/hack/map_abi_test_type_go125_test.go new file mode 100644 index 0000000000000..ef58a5f58d970 --- /dev/null +++ b/pkg/util/hack/map_abi_test_type_go125_test.go @@ -0,0 +1,21 @@ +// Copyright 2025 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build go1.25 && !go1.26 + +package hack + +type testMapTable = swissMapTable + +const testMapGroupSlots = swissMapGroupSlots diff --git a/pkg/util/hack/map_abi_test_type_go126_test.go b/pkg/util/hack/map_abi_test_type_go126_test.go new file mode 100644 index 0000000000000..a54a15d76e961 --- /dev/null +++ b/pkg/util/hack/map_abi_test_type_go126_test.go @@ -0,0 +1,21 @@ +// Copyright 2025 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build go1.26 && !go1.27 + +package hack + +type testMapTable = mapTable + +const testMapGroupSlots = mapGroupSlots diff --git a/tools/check/bazel-check-abi.sh b/tools/check/bazel-check-abi.sh index 07765d614cd71..46c905f80f060 100755 --- a/tools/check/bazel-check-abi.sh +++ b/tools/check/bazel-check-abi.sh @@ -16,21 +16,41 @@ set -euo pipefail GOROOT=$(bazel run @io_bazel_rules_go//go -- env GOROOT) -cd ${GOROOT} +GOVERSION=$(bazel run @io_bazel_rules_go//go -- env GOVERSION) gosrc_md5=() -gosrc_md5+=("src/internal/runtime/maps/map.go a29531cd3447fd3c90ceabfde5a08921") -gosrc_md5+=("src/internal/runtime/maps/table.go 1ff4f281722eb83ac7d64ae0453e9718") -gosrc_md5+=("src/internal/abi/map_swiss.go 7ef614406774c5be839e63aea0225b00") -gosrc_md5+=("src/internal/abi/type.go d0caafb471a5b971854ca6426510608c") +case "${GOVERSION}" in + go1.25.*) + gosrc_md5+=("src/internal/runtime/maps/map.go a29531cd3447fd3c90ceabfde5a08921") + gosrc_md5+=("src/internal/runtime/maps/table.go 1ff4f281722eb83ac7d64ae0453e9718") + gosrc_md5+=("src/internal/abi/map_swiss.go 7ef614406774c5be839e63aea0225b00") + gosrc_md5+=("src/internal/abi/type.go d0caafb471a5b971854ca6426510608c") + ;; + go1.26.*) + gosrc_md5+=("src/internal/runtime/maps/map.go 3e68f31ef3238e389fdac4054fc8704e") + gosrc_md5+=("src/internal/runtime/maps/table.go 30d2976dac7b7033b62b6362e2b6d557") + gosrc_md5+=("src/internal/abi/map.go 4e5fb78cb88f2edc634fa040f99dcacc") + gosrc_md5+=("src/internal/abi/type.go 3bd3a7cef23f68f1bc98b8a40551a425") + ;; + *) + echo "Unsupported Go version for ABI check: ${GOVERSION}. Expected go1.25.x or go1.26.x." + exit 1 + ;; +esac for x in "${gosrc_md5[@]}"; do x=($x) src="${x[0]}" md5="${x[1]}" echo "Checking ${src}" - if [ $(md5sum "${src}" | cut -d' ' -f1) != "${md5}" ]; then - echo "Unexpect checksum for ${src}" - exit -1 + src_path="${GOROOT}/${src}" + if [ ! -f "${src_path}" ]; then + echo "Missing source file: ${src_path}" + exit 1 + fi + actual_md5=$(md5sum "${src_path}" | cut -d' ' -f1) + if [ "${actual_md5}" != "${md5}" ]; then + echo "Unexpected checksum for ${src}: expected ${md5}, got ${actual_md5}" + exit 1 fi done