From 23b5a7ec7cd2e8aa3712f9a278001a967706291c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Apr 2026 19:47:02 +0000 Subject: [PATCH 01/21] Update buildkit-proto to BuildKit v0.29.0 Bumps the BUILDKIT_VERSION pinned in update.sh from v0.18 to v0.29.0 and refreshes every .proto file by re-running the script. Also adds the third-party schemas the script already references (sourcepolicy, vtprotobuf, google.rpc, google.protobuf), which were missing on disk and required by the gateway/fsutil imports. `cargo build -p buildkit-proto` succeeds. https://claude.ai/code/session_01XtZHcL6rKJDuX7tUS3okdc --- .../moby/buildkit/api/types/worker.proto | 8 + .../frontend/gateway/pb/gateway.proto | 80 + .../moby/buildkit/solver/pb/ops.proto | 26 + .../buildkit/sourcepolicy/pb/policy.proto | 66 + .../planetscale/vtprotobuf/vtproto/ext.proto | 23 + .../proto/google/protobuf/any.proto | 106 ++ .../proto/google/protobuf/descriptor.proto | 1476 +++++++++++++++++ buildkit-proto/proto/google/rpc/status.proto | 48 + buildkit-proto/update.sh | 2 +- 9 files changed, 1834 insertions(+), 1 deletion(-) create mode 100644 buildkit-proto/proto/github.com/moby/buildkit/sourcepolicy/pb/policy.proto create mode 100644 buildkit-proto/proto/github.com/planetscale/vtprotobuf/vtproto/ext.proto create mode 100644 buildkit-proto/proto/google/protobuf/any.proto create mode 100644 buildkit-proto/proto/google/protobuf/descriptor.proto create mode 100644 buildkit-proto/proto/google/rpc/status.proto diff --git a/buildkit-proto/proto/github.com/moby/buildkit/api/types/worker.proto b/buildkit-proto/proto/github.com/moby/buildkit/api/types/worker.proto index 51a92d6..8f56566 100644 --- a/buildkit-proto/proto/github.com/moby/buildkit/api/types/worker.proto +++ b/buildkit-proto/proto/github.com/moby/buildkit/api/types/worker.proto @@ -12,6 +12,7 @@ message WorkerRecord { repeated pb.Platform platforms = 3; repeated GCPolicy GCPolicy = 4; BuildkitVersion BuildkitVersion = 5; + repeated CDIDevice CDIDevices = 6; } message GCPolicy { @@ -30,3 +31,10 @@ message BuildkitVersion { string version = 2; string revision = 3; } + +message CDIDevice { + string Name = 1; + bool AutoAllow = 2; + map Annotations = 3; + bool OnDemand = 4; +} \ No newline at end of file diff --git a/buildkit-proto/proto/github.com/moby/buildkit/frontend/gateway/pb/gateway.proto b/buildkit-proto/proto/github.com/moby/buildkit/frontend/gateway/pb/gateway.proto index cb2b333..6480778 100644 --- a/buildkit-proto/proto/github.com/moby/buildkit/frontend/gateway/pb/gateway.proto +++ b/buildkit-proto/proto/github.com/moby/buildkit/frontend/gateway/pb/gateway.proto @@ -9,6 +9,7 @@ import "github.com/moby/buildkit/solver/pb/ops.proto"; import "github.com/moby/buildkit/sourcepolicy/pb/policy.proto"; import "github.com/moby/buildkit/util/apicaps/pb/caps.proto"; import "github.com/tonistiigi/fsutil/types/stat.proto"; +import "google/protobuf/timestamp.proto"; import "google/rpc/status.proto"; service LLBBridge { @@ -35,6 +36,11 @@ service LLBBridge { rpc ReleaseContainer(ReleaseContainerRequest) returns (ReleaseContainerResponse); rpc ExecProcess(stream ExecMessage) returns (stream ExecMessage); + // apicaps:CapGatewayExecFilesystem + rpc ReadFileContainer(ReadFileRequest) returns (ReadFileResponse); + rpc ReadDirContainer(ReadDirRequest) returns (ReadDirResponse); + rpc StatFileContainer(StatFileRequest) returns (StatFileResponse); + // apicaps:CapGatewayWarnings rpc Warn(WarnRequest) returns (WarnResponse); } @@ -134,17 +140,76 @@ message ResolveSourceMetaRequest { pb.Platform Platform = 2; string LogName = 3; string ResolveMode = 4; + ResolveSourceGitRequest Git = 5; + ResolveSourceImageRequest Image = 6; + ResolveSourceHTTPRequest HTTP = 7; repeated moby.buildkit.v1.sourcepolicy.Policy SourcePolicies = 8; } message ResolveSourceMetaResponse { pb.SourceOp Source = 1; ResolveSourceImageResponse Image = 2; + ResolveSourceGitResponse Git = 3; + ResolveSourceHTTPResponse HTTP = 4; +} + +message ResolveSourceImageRequest { + bool NoConfig = 1; + bool AttestationChain = 2; + repeated string ResolveAttestations = 3; +} + +message AttestationChain { + string Root = 1; + string ImageManifest = 2; + string AttestationManifest = 3; + repeated string SignatureManifests = 4; + map Blobs = 5; } message ResolveSourceImageResponse { string Digest = 1; bytes Config = 2; + AttestationChain AttestationChain = 3; +} + +message ResolveSourceGitRequest { + // Return full commit and tag object bytes. + bool ReturnObject = 1; +} + +message ResolveSourceGitResponse { + string Checksum = 1; + string Ref = 2; + string CommitChecksum = 3; + bytes CommitObject = 4; + bytes TagObject = 5; +} + +message ResolveSourceHTTPResponse { + string Checksum = 1; + string Filename = 2; + google.protobuf.Timestamp LastModified = 3; + ChecksumResponse ChecksumResponse = 4; +} + +message ResolveSourceHTTPRequest { + ChecksumRequest ChecksumRequest = 1; +} + +message ChecksumRequest { + enum ChecksumAlgo { + CHECKSUM_ALGO_SHA256 = 0; + CHECKSUM_ALGO_SHA384 = 1; + CHECKSUM_ALGO_SHA512 = 2; + } + ChecksumAlgo Algo = 1; + bytes Suffix = 2; +} + +message ChecksumResponse { + string Digest = 1; + bytes Suffix = 2; } message SolveRequest { @@ -190,6 +255,7 @@ message ReadFileRequest { string Ref = 1; string FilePath = 2; FileRange Range = 3; + int32 MountIndex = 4; } message FileRange { @@ -205,6 +271,7 @@ message ReadDirRequest { string Ref = 1; string DirPath = 2; string IncludePattern = 3; + int32 MountIndex = 4; } message ReadDirResponse { @@ -214,6 +281,7 @@ message ReadDirResponse { message StatFileRequest { string Ref = 1; string Path = 2; + int32 MountIndex = 3; } message StatFileResponse { @@ -325,3 +393,15 @@ message SignalMessage { // are platform dependent. string Name = 1; } + +message Blob { + Descriptor descriptor = 1; + bytes data = 2; +} + +message Descriptor { + string media_type = 1; + string digest = 2; + int64 size = 3; + map annotations = 5; +} diff --git a/buildkit-proto/proto/github.com/moby/buildkit/solver/pb/ops.proto b/buildkit-proto/proto/github.com/moby/buildkit/solver/pb/ops.proto index f1432af..731123b 100644 --- a/buildkit-proto/proto/github.com/moby/buildkit/solver/pb/ops.proto +++ b/buildkit-proto/proto/github.com/moby/buildkit/solver/pb/ops.proto @@ -47,6 +47,7 @@ message ExecOp { NetMode network = 3; SecurityMode security = 4; repeated SecretEnv secretenv = 5; + repeated CDIDevice cdiDevices = 6; } // Meta is a set of arguments for ExecOp. @@ -95,6 +96,15 @@ message SecretEnv { bool optional = 3; } +// CDIDevice specifies a CDI device information. +message CDIDevice { + // Fully qualified CDI device name (e.g., vendor.com/gpu=gpudevice1) + // https://github.com/cncf-tags/container-device-interface/blob/main/SPEC.md + string name = 1; + // Optional defines if CDI device is required. + bool optional = 2; +} + // Mount specifies how to mount an input Op as a filesystem. message Mount { int64 input = 1; @@ -309,6 +319,8 @@ message FileAction { FileActionMkDir mkdir = 6; // FileActionRm removes a file FileActionRm rm = 7; + // FileActionSymlink creates a symlink + FileActionSymlink symlink = 8; } } @@ -343,6 +355,9 @@ message FileActionCopy { bool alwaysReplaceExistingDestPaths = 14; // mode in non-octal format string modeStr = 15; + // required paths that must be included in the copy. This is only used when + // include_patterns has at least one pattern. + repeated string required_paths = 16; } message FileActionMkFile { @@ -358,6 +373,17 @@ message FileActionMkFile { int64 timestamp = 5; } +message FileActionSymlink { + // destination path for the new file representing the link + string oldpath = 1; + // source path for the link + string newpath = 2; + // optional owner for the new file + ChownOpt owner = 3; + // optional created time override + int64 timestamp = 4; +} + message FileActionMkDir { // path for the new directory string path = 1; diff --git a/buildkit-proto/proto/github.com/moby/buildkit/sourcepolicy/pb/policy.proto b/buildkit-proto/proto/github.com/moby/buildkit/sourcepolicy/pb/policy.proto new file mode 100644 index 0000000..ca3cac4 --- /dev/null +++ b/buildkit-proto/proto/github.com/moby/buildkit/sourcepolicy/pb/policy.proto @@ -0,0 +1,66 @@ +syntax = "proto3"; + +package moby.buildkit.v1.sourcepolicy; + +option go_package = "github.com/moby/buildkit/sourcepolicy/pb;moby_buildkit_v1_sourcepolicy"; + +// Rule defines the action(s) to take when a source is matched +message Rule { + PolicyAction action = 1; + Selector selector = 2; + Update updates = 3; +} + +// Update contains updates to the matched build step after rule is applied +message Update { + string identifier = 1; + map attrs = 2; +} + +// Selector identifies a source to match a policy to +message Selector { + string identifier = 1; + // MatchType is the type of match to perform on the source identifier + MatchType match_type = 2; + repeated AttrConstraint constraints = 3; +} + +// PolicyAction defines the action to take when a source is matched +enum PolicyAction { + ALLOW = 0; + DENY = 1; + CONVERT = 2; +} + +// AttrConstraint defines a constraint on a source attribute +message AttrConstraint { + string key = 1; + string value = 2; + AttrMatch condition = 3; +} + +// AttrMatch defines the condition to match a source attribute +enum AttrMatch { + EQUAL = 0; + NOTEQUAL = 1; + MATCHES = 2; +} + +// Policy is the list of rules the policy engine will perform +message Policy { + int64 version = 1; // Currently 1 + repeated Rule rules = 2; +} + +// Match type is used to determine how a rule source is matched +enum MatchType { + // WILDCARD is the default matching type. + // It may first attempt to due an exact match but will follow up with a wildcard match + // For something more powerful, use REGEX + WILDCARD = 0; + // EXACT treats the source identifier as a litteral string match + EXACT = 1; + // REGEX treats the source identifier as a regular expression + // With regex matching you can also use match groups to replace values in the destination identifier + REGEX = 2; +} diff --git a/buildkit-proto/proto/github.com/planetscale/vtprotobuf/vtproto/ext.proto b/buildkit-proto/proto/github.com/planetscale/vtprotobuf/vtproto/ext.proto new file mode 100644 index 0000000..21d7810 --- /dev/null +++ b/buildkit-proto/proto/github.com/planetscale/vtprotobuf/vtproto/ext.proto @@ -0,0 +1,23 @@ +syntax = "proto2"; +package vtproto; + +import "google/protobuf/descriptor.proto"; + +option java_package = "com.google.protobuf"; +option java_outer_classname = "VTProto"; +option go_package = "github.com/planetscale/vtprotobuf/vtproto"; + +extend google.protobuf.MessageOptions { + optional bool mempool = 64101; + optional bool ignore_unknown_fields = 64102; +} + +extend google.protobuf.FieldOptions { + optional Opts options = 64150; +} + +// These options should be used during schema definition, +// applying them to some of the fields in protobuf +message Opts { + optional bool unique = 1; +} diff --git a/buildkit-proto/proto/google/protobuf/any.proto b/buildkit-proto/proto/google/protobuf/any.proto new file mode 100644 index 0000000..e95b5b4 --- /dev/null +++ b/buildkit-proto/proto/google/protobuf/any.proto @@ -0,0 +1,106 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +syntax = "proto3"; + +package google.protobuf; + +option go_package = "google.golang.org/protobuf/types/known/anypb"; +option java_package = "com.google.protobuf"; +option java_outer_classname = "AnyProto"; +option java_multiple_files = true; +option objc_class_prefix = "GPB"; +option csharp_namespace = "Google.Protobuf.WellKnownTypes"; + +// `Any` contains an arbitrary serialized protocol buffer message along with a +// URL that describes the type of the serialized message. +// +// In its binary encoding, an `Any` is an ordinary message; but in other wire +// forms like JSON, it has a special encoding. The format of the type URL is +// described on the `type_url` field. +// +// Protobuf APIs provide utilities to interact with `Any` values: +// +// - A 'pack' operation accepts a message and constructs a generic `Any` wrapper +// around it. +// - An 'unpack' operation reads the content of an `Any` message, either into an +// existing message or a new one. Unpack operations must check the type of the +// value they unpack against the declared `type_url`. +// - An 'is' operation decides whether an `Any` contains a message of the given +// type, i.e. whether it can 'unpack' that type. +// +// The JSON format representation of an `Any` follows one of these cases: +// +// - For types without special-cased JSON encodings, the JSON format +// representation of the `Any` is the same as that of the message, with an +// additional `@type` field which contains the type URL. +// - For types with special-cased JSON encodings (typically called 'well-known' +// types, listed in https://protobuf.dev/programming-guides/json/#any), the +// JSON format representation has a key `@type` which contains the type URL +// and a key `value` which contains the JSON-serialized value. +// +// The text format representation of an `Any` is like a message with one field +// whose name is the type URL in brackets. For example, an `Any` containing a +// `foo.Bar` message may be written `[type.googleapis.com/foo.Bar] { a: 2 }`. +message Any { + // Identifies the type of the serialized Protobuf message with a URI reference + // consisting of a prefix ending in a slash and the fully-qualified type name. + // + // Example: type.googleapis.com/google.protobuf.StringValue + // + // This string must contain at least one `/` character, and the content after + // the last `/` must be the fully-qualified name of the type in canonical + // form, without a leading dot. Do not write a scheme on these URI references + // so that clients do not attempt to contact them. + // + // The prefix is arbitrary and Protobuf implementations are expected to + // simply strip off everything up to and including the last `/` to identify + // the type. `type.googleapis.com/` is a common default prefix that some + // legacy implementations require. This prefix does not indicate the origin of + // the type, and URIs containing it are not expected to respond to any + // requests. + // + // All type URL strings must be legal URI references with the additional + // restriction (for the text format) that the content of the reference + // must consist only of alphanumeric characters, percent-encoded escapes, and + // characters in the following set (not including the outer backticks): + // `/-.~_!$&()*+,;=`. Despite our allowing percent encodings, implementations + // should not unescape them to prevent confusion with existing parsers. For + // example, `type.googleapis.com%2FFoo` should be rejected. + // + // In the original design of `Any`, the possibility of launching a type + // resolution service at these type URLs was considered but Protobuf never + // implemented one and considers contacting these URLs to be problematic and + // a potential security issue. Do not attempt to contact type URLs. + string type_url = 1; + + // Holds a Protobuf serialization of the type described by type_url. + bytes value = 2; +} diff --git a/buildkit-proto/proto/google/protobuf/descriptor.proto b/buildkit-proto/proto/google/protobuf/descriptor.proto new file mode 100644 index 0000000..671237a --- /dev/null +++ b/buildkit-proto/proto/google/protobuf/descriptor.proto @@ -0,0 +1,1476 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// Author: kenton@google.com (Kenton Varda) +// Based on original Protocol Buffers design by +// Sanjay Ghemawat, Jeff Dean, and others. +// +// The messages in this file describe the definitions found in .proto files. +// A valid .proto file can be translated directly to a FileDescriptorProto +// without any other information (e.g. without reading its imports). + +syntax = "proto2"; + +package google.protobuf; + +option go_package = "google.golang.org/protobuf/types/descriptorpb"; +option java_package = "com.google.protobuf"; +option java_outer_classname = "DescriptorProtos"; +option csharp_namespace = "Google.Protobuf.Reflection"; +option objc_class_prefix = "GPB"; +option cc_enable_arenas = true; + +// descriptor.proto must be optimized for speed because reflection-based +// algorithms don't work during bootstrapping. +option optimize_for = SPEED; + +// The protocol compiler can output a FileDescriptorSet containing the .proto +// files it parses. +message FileDescriptorSet { + repeated FileDescriptorProto file = 1; + + // Extensions for tooling. + extensions 536000000 [declaration = { + number: 536000000 + type: ".buf.descriptor.v1.FileDescriptorSetExtension" + full_name: ".buf.descriptor.v1.buf_file_descriptor_set_extension" + }]; +} + +// The full set of known editions. +enum Edition { + // A placeholder for an unknown edition value. + EDITION_UNKNOWN = 0; + + // A placeholder edition for specifying default behaviors *before* a feature + // was first introduced. This is effectively an "infinite past". + EDITION_LEGACY = 900; + + // Legacy syntax "editions". These pre-date editions, but behave much like + // distinct editions. These can't be used to specify the edition of proto + // files, but feature definitions must supply proto2/proto3 defaults for + // backwards compatibility. + EDITION_PROTO2 = 998; + EDITION_PROTO3 = 999; + + // Editions that have been released. The specific values are arbitrary and + // should not be depended on, but they will always be time-ordered for easy + // comparison. + EDITION_2023 = 1000; + EDITION_2024 = 1001; + EDITION_2026 = 1002; + + // A placeholder edition for developing and testing unscheduled features. + EDITION_UNSTABLE = 9999; + + // Placeholder editions for testing feature resolution. These should not be + // used or relied on outside of tests. + EDITION_1_TEST_ONLY = 1; + EDITION_2_TEST_ONLY = 2; + EDITION_99997_TEST_ONLY = 99997; + EDITION_99998_TEST_ONLY = 99998; + EDITION_99999_TEST_ONLY = 99999; + + // Placeholder for specifying unbounded edition support. This should only + // ever be used by plugins that can expect to never require any changes to + // support a new edition. + EDITION_MAX = 0x7FFFFFFF; +} + +// Describes a complete .proto file. +message FileDescriptorProto { + optional string name = 1; // file name, relative to root of source tree + optional string package = 2; // e.g. "foo", "foo.bar", etc. + + // Names of files imported by this file. + repeated string dependency = 3; + // Indexes of the public imported files in the dependency list above. + repeated int32 public_dependency = 10; + // Indexes of the weak imported files in the dependency list. + // For Google-internal migration only. Do not use. + repeated int32 weak_dependency = 11; + + // Names of files imported by this file purely for the purpose of providing + // option extensions. These are excluded from the dependency list above. + repeated string option_dependency = 15; + + // All top-level definitions in this file. + repeated DescriptorProto message_type = 4; + repeated EnumDescriptorProto enum_type = 5; + repeated ServiceDescriptorProto service = 6; + repeated FieldDescriptorProto extension = 7; + + optional FileOptions options = 8; + + // This field contains optional information about the original source code. + // You may safely remove this entire field without harming runtime + // functionality of the descriptors -- the information is needed only by + // development tools. + optional SourceCodeInfo source_code_info = 9; + + // The syntax of the proto file. + // The supported values are "proto2", "proto3", and "editions". + // + // If `edition` is present, this value must be "editions". + // WARNING: This field should only be used by protobuf plugins or special + // cases like the proto compiler. Other uses are discouraged and + // developers should rely on the protoreflect APIs for their client language. + optional string syntax = 12; + + // The edition of the proto file. + // WARNING: This field should only be used by protobuf plugins or special + // cases like the proto compiler. Other uses are discouraged and + // developers should rely on the protoreflect APIs for their client language. + optional Edition edition = 14; +} + +// Describes a message type. +message DescriptorProto { + optional string name = 1; + + repeated FieldDescriptorProto field = 2; + repeated FieldDescriptorProto extension = 6; + + repeated DescriptorProto nested_type = 3; + repeated EnumDescriptorProto enum_type = 4; + + message ExtensionRange { + optional int32 start = 1; // Inclusive. + optional int32 end = 2; // Exclusive. + + optional ExtensionRangeOptions options = 3; + } + repeated ExtensionRange extension_range = 5; + + repeated OneofDescriptorProto oneof_decl = 8; + + optional MessageOptions options = 7; + + // Range of reserved tag numbers. Reserved tag numbers may not be used by + // fields or extension ranges in the same message. Reserved ranges may + // not overlap. + message ReservedRange { + optional int32 start = 1; // Inclusive. + optional int32 end = 2; // Exclusive. + } + repeated ReservedRange reserved_range = 9; + // Reserved field names, which may not be used by fields in the same message. + // A given name may only be reserved once. + repeated string reserved_name = 10; + + // Support for `export` and `local` keywords on enums. + optional SymbolVisibility visibility = 11; +} + +message ExtensionRangeOptions { + // Range reserved for first-class custom options defined by the Protobuf + // team. User custom options must use the 1000+ range instead. + extensions 990 to 998; + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + message Declaration { + // The extension number declared within the extension range. + optional int32 number = 1; + + // The fully-qualified name of the extension field. There must be a leading + // dot in front of the full name. + optional string full_name = 2; + + // The fully-qualified type name of the extension field. Unlike + // Metadata.type, Declaration.type must have a leading dot for messages + // and enums. + optional string type = 3; + + // If true, indicates that the number is reserved in the extension range, + // and any extension field with the number will fail to compile. Set this + // when a declared extension field is deleted. + optional bool reserved = 5; + + // If true, indicates that the extension must be defined as repeated. + // Otherwise the extension must be defined as optional. + optional bool repeated = 6; + + reserved 4; // removed is_repeated + } + + // For external users: DO NOT USE. We are in the process of open sourcing + // extension declaration and executing internal cleanups before it can be + // used externally. + repeated Declaration declaration = 2 [retention = RETENTION_SOURCE]; + + // Any features defined in the specific edition. + optional FeatureSet features = 50; + + // The verification state of the extension range. + enum VerificationState { + // All the extensions of the range must be declared. + DECLARATION = 0; + UNVERIFIED = 1; + } + + // The verification state of the range. + // TODO: flip the default to DECLARATION once all empty ranges + // are marked as UNVERIFIED. + optional VerificationState verification = 3 + [default = UNVERIFIED, retention = RETENTION_SOURCE]; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +// Describes a field within a message. +message FieldDescriptorProto { + enum Type { + // 0 is reserved for errors. + // Order is weird for historical reasons. + TYPE_DOUBLE = 1; + TYPE_FLOAT = 2; + // Not ZigZag encoded. Negative numbers take 10 bytes. Use TYPE_SINT64 if + // negative values are likely. + TYPE_INT64 = 3; + TYPE_UINT64 = 4; + // Not ZigZag encoded. Negative numbers take 10 bytes. Use TYPE_SINT32 if + // negative values are likely. + TYPE_INT32 = 5; + TYPE_FIXED64 = 6; + TYPE_FIXED32 = 7; + TYPE_BOOL = 8; + TYPE_STRING = 9; + // Tag-delimited aggregate. + // Group type is deprecated and not supported after google.protobuf. However, Proto3 + // implementations should still be able to parse the group wire format and + // treat group fields as unknown fields. In Editions, the group wire format + // can be enabled via the `message_encoding` feature. + TYPE_GROUP = 10; + TYPE_MESSAGE = 11; // Length-delimited aggregate. + + // New in version 2. + TYPE_BYTES = 12; + TYPE_UINT32 = 13; + TYPE_ENUM = 14; + TYPE_SFIXED32 = 15; + TYPE_SFIXED64 = 16; + TYPE_SINT32 = 17; // Uses ZigZag encoding. + TYPE_SINT64 = 18; // Uses ZigZag encoding. + } + + enum Label { + // 0 is reserved for errors + LABEL_OPTIONAL = 1; + LABEL_REPEATED = 3; + // The required label is only allowed in google.protobuf. In proto3 and Editions + // it's explicitly prohibited. In Editions, the `field_presence` feature + // can be used to get this behavior. + LABEL_REQUIRED = 2; + } + + optional string name = 1; + optional int32 number = 3; + optional Label label = 4; + + // If type_name is set, this need not be set. If both this and type_name + // are set, this must be one of TYPE_ENUM, TYPE_MESSAGE or TYPE_GROUP. + optional Type type = 5; + + // For message and enum types, this is the name of the type. If the name + // starts with a '.', it is fully-qualified. Otherwise, C++-like scoping + // rules are used to find the type (i.e. first the nested types within this + // message are searched, then within the parent, on up to the root + // namespace). + optional string type_name = 6; + + // For extensions, this is the name of the type being extended. It is + // resolved in the same manner as type_name. + optional string extendee = 2; + + // For numeric types, contains the original text representation of the value. + // For booleans, "true" or "false". + // For strings, contains the default text contents (not escaped in any way). + // For bytes, contains the C escaped value. All bytes >= 128 are escaped. + optional string default_value = 7; + + // If set, gives the index of a oneof in the containing type's oneof_decl + // list. This field is a member of that oneof. + optional int32 oneof_index = 9; + + // JSON name of this field. The value is set by protocol compiler. If the + // user has set a "json_name" option on this field, that option's value + // will be used. Otherwise, it's deduced from the field's name by converting + // it to camelCase. + optional string json_name = 10; + + optional FieldOptions options = 8; + + // If true, this is a proto3 "optional". When a proto3 field is optional, it + // tracks presence regardless of field type. + // + // When proto3_optional is true, this field must belong to a oneof to signal + // to old proto3 clients that presence is tracked for this field. This oneof + // is known as a "synthetic" oneof, and this field must be its sole member + // (each proto3 optional field gets its own synthetic oneof). Synthetic oneofs + // exist in the descriptor only, and do not generate any API. Synthetic oneofs + // must be ordered after all "real" oneofs. + // + // For message fields, proto3_optional doesn't create any semantic change, + // since non-repeated message fields always track presence. However it still + // indicates the semantic detail of whether the user wrote "optional" or not. + // This can be useful for round-tripping the .proto file. For consistency we + // give message fields a synthetic oneof also, even though it is not required + // to track presence. This is especially important because the parser can't + // tell if a field is a message or an enum, so it must always create a + // synthetic oneof. + // + // Proto2 optional fields do not set this flag, because they already indicate + // optional with `LABEL_OPTIONAL`. + optional bool proto3_optional = 17; +} + +// Describes a oneof. +message OneofDescriptorProto { + optional string name = 1; + optional OneofOptions options = 2; +} + +// Describes an enum type. +message EnumDescriptorProto { + optional string name = 1; + + repeated EnumValueDescriptorProto value = 2; + + optional EnumOptions options = 3; + + // Range of reserved numeric values. Reserved values may not be used by + // entries in the same enum. Reserved ranges may not overlap. + // + // Note that this is distinct from DescriptorProto.ReservedRange in that it + // is inclusive such that it can appropriately represent the entire int32 + // domain. + message EnumReservedRange { + optional int32 start = 1; // Inclusive. + optional int32 end = 2; // Inclusive. + } + + // Range of reserved numeric values. Reserved numeric values may not be used + // by enum values in the same enum declaration. Reserved ranges may not + // overlap. + repeated EnumReservedRange reserved_range = 4; + + // Reserved enum value names, which may not be reused. A given name may only + // be reserved once. + repeated string reserved_name = 5; + + // Support for `export` and `local` keywords on enums. + optional SymbolVisibility visibility = 6; +} + +// Describes a value within an enum. +message EnumValueDescriptorProto { + optional string name = 1; + optional int32 number = 2; + + optional EnumValueOptions options = 3; +} + +// Describes a service. +message ServiceDescriptorProto { + optional string name = 1; + repeated MethodDescriptorProto method = 2; + + optional ServiceOptions options = 3; + + reserved 4; + reserved "stream"; +} + +// Describes a method of a service. +message MethodDescriptorProto { + optional string name = 1; + + // Input and output type names. These are resolved in the same way as + // FieldDescriptorProto.type_name, but must refer to a message type. + optional string input_type = 2; + optional string output_type = 3; + + optional MethodOptions options = 4; + + // Identifies if client streams multiple client messages + optional bool client_streaming = 5 [default = false]; + // Identifies if server streams multiple server messages + optional bool server_streaming = 6 [default = false]; +} + +// =================================================================== +// Options + +// Each of the definitions above may have "options" attached. These are +// just annotations which may cause code to be generated slightly differently +// or may contain hints for code that manipulates protocol messages. +// +// Clients may define custom options as extensions of the *Options messages. +// These extensions may not yet be known at parsing time, so the parser cannot +// store the values in them. Instead it stores them in a field in the *Options +// message called uninterpreted_option. This field must have the same name +// across all *Options messages. We then use this field to populate the +// extensions when we build a descriptor, at which point all protos have been +// parsed and so all extensions are known. +// +// Extension numbers for custom options may be chosen as follows: +// * For options which will only be used within a single application or +// organization, or for experimental options, use field numbers 50000 +// through 99999. It is up to you to ensure that you do not use the +// same number for multiple options. +// * For options which will be published and used publicly by multiple +// independent entities, e-mail protobuf-global-extension-registry@google.com +// to reserve extension numbers. Simply provide your project name (e.g. +// Objective-C plugin) and your project website (if available) -- there's no +// need to explain how you intend to use them. Usually you only need one +// extension number. You can declare multiple options with only one extension +// number by putting them in a sub-message. See the Custom Options section of +// the docs for examples: +// https://developers.google.com/protocol-buffers/docs/proto#options +// If this turns out to be popular, a web service will be set up +// to automatically assign option numbers. + +message FileOptions { + + // Sets the Java package where classes generated from this .proto will be + // placed. By default, the proto package is used, but this is often + // inappropriate because proto packages do not normally start with backwards + // domain names. + optional string java_package = 1; + + // Controls the name of the wrapper Java class generated for the .proto file. + // That class will always contain the .proto file's getDescriptor() method as + // well as any top-level extensions defined in the .proto file. + // If java_multiple_files is disabled, then all the other classes from the + // .proto file will be nested inside the single wrapper outer class. + optional string java_outer_classname = 8; + + // If enabled, then the Java code generator will generate a separate .java + // file for each top-level message, enum, and service defined in the .proto + // file. Thus, these types will *not* be nested inside the wrapper class + // named by java_outer_classname. However, the wrapper class will still be + // generated to contain the file's getDescriptor() method as well as any + // top-level extensions defined in the file. + optional bool java_multiple_files = 10 [ + default = false, + feature_support = { + edition_introduced: EDITION_PROTO2 + edition_removed: EDITION_2024 + removal_error: "This behavior is enabled by default in editions 2024 and above. " + "To disable it, you can set `features.(pb.java).nest_in_file_class = YES` " + "on individual messages, enums, or services." + + } + ]; + + // This option does nothing. + optional bool java_generate_equals_and_hash = 20 [deprecated=true]; + + // A proto2 file can set this to true to opt in to UTF-8 checking for Java, + // which will throw an exception if invalid UTF-8 is parsed from the wire or + // assigned to a string field. + // + // TODO: clarify exactly what kinds of field types this option + // applies to, and update these docs accordingly. + // + // Proto3 files already perform these checks. Setting the option explicitly to + // false has no effect: it cannot be used to opt proto3 files out of UTF-8 + // checks. + optional bool java_string_check_utf8 = 27 [default = false]; + + // Generated classes can be optimized for speed or code size. + enum OptimizeMode { + SPEED = 1; // Generate complete code for parsing, serialization, + // etc. + CODE_SIZE = 2; // Use ReflectionOps to implement these methods. + LITE_RUNTIME = 3; // Generate code using MessageLite and the lite runtime. + } + optional OptimizeMode optimize_for = 9 [default = SPEED]; + + // Sets the Go package where structs generated from this .proto will be + // placed. If omitted, the Go package will be derived from the following: + // - The basename of the package import path, if provided. + // - Otherwise, the package statement in the .proto file, if present. + // - Otherwise, the basename of the .proto file, without extension. + optional string go_package = 11; + + // Should generic services be generated in each language? "Generic" services + // are not specific to any particular RPC system. They are generated by the + // main code generators in each language (without additional plugins). + // Generic services were the only kind of service generation supported by + // early versions of google.protobuf. + // + // Generic services are now considered deprecated in favor of using plugins + // that generate code specific to your particular RPC system. Therefore, + // these default to false. Old code which depends on generic services should + // explicitly set them to true. + optional bool cc_generic_services = 16 [default = false]; + optional bool java_generic_services = 17 [default = false]; + optional bool py_generic_services = 18 [default = false]; + reserved 42; // removed php_generic_services + reserved "php_generic_services"; + + // Is this file deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for everything in the file, or it will be completely ignored; in the very + // least, this is a formalization for deprecating files. + optional bool deprecated = 23 [default = false]; + + // Enables the use of arenas for the proto messages in this file. This applies + // only to generated classes for C++. + optional bool cc_enable_arenas = 31 [default = true]; + + // Sets the objective c class prefix which is prepended to all objective c + // generated classes from this .proto. There is no default. + optional string objc_class_prefix = 36; + + // Namespace for generated classes; defaults to the package. + optional string csharp_namespace = 37; + + // By default Swift generators will take the proto package and CamelCase it + // replacing '.' with underscore and use that to prefix the types/symbols + // defined. When this options is provided, they will use this value instead + // to prefix the types/symbols defined. + optional string swift_prefix = 39; + + // Sets the php class prefix which is prepended to all php generated classes + // from this .proto. Default is empty. + optional string php_class_prefix = 40; + + // Use this option to change the namespace of php generated classes. Default + // is empty. When this option is empty, the package name will be used for + // determining the namespace. + optional string php_namespace = 41; + + // Use this option to change the namespace of php generated metadata classes. + // Default is empty. When this option is empty, the proto file name will be + // used for determining the namespace. + optional string php_metadata_namespace = 44; + + // Use this option to change the package of ruby generated classes. Default + // is empty. When this option is not set, the package name will be used for + // determining the ruby package. + optional string ruby_package = 45; + + // Any features defined in the specific edition. + // WARNING: This field should only be used by protobuf plugins or special + // cases like the proto compiler. Other uses are discouraged and + // developers should rely on the protoreflect APIs for their client language. + optional FeatureSet features = 50; + + // Range reserved for first-class custom options defined by the Protobuf + // team. User custom options must use the 1000+ range instead. + extensions 990 to 998 [declaration = { + number: 990, + full_name: ".pb.file.cpp", + type: ".pb.file.CppFileOptions" + }]; + + // The parser stores options it doesn't recognize here. + // See the documentation for the "Options" section above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. + // See the documentation for the "Options" section above. + extensions 1000 to max; + + reserved 38; +} + +message MessageOptions { + // Set true to use the old proto1 MessageSet wire format for extensions. + // This is provided for backwards-compatibility with the MessageSet wire + // format. You should not use this for any other reason: It's less + // efficient, has fewer features, and is more complicated. + // + // The message must be defined exactly as follows: + // message Foo { + // option message_set_wire_format = true; + // extensions 4 to max; + // } + // Note that the message cannot have any defined fields; MessageSets only + // have extensions. + // + // All extensions of your type must be singular messages; e.g. they cannot + // be int32s, enums, or repeated messages. + // + // Because this is an option, the above two restrictions are not enforced by + // the protocol compiler. + optional bool message_set_wire_format = 1 [default = false]; + + // Disables the generation of the standard "descriptor()" accessor, which can + // conflict with a field of the same name. This is meant to make migration + // from proto1 easier; new code should avoid fields named "descriptor". + optional bool no_standard_descriptor_accessor = 2 [default = false]; + + // Is this message deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for the message, or it will be completely ignored; in the very least, + // this is a formalization for deprecating messages. + optional bool deprecated = 3 [default = false]; + + reserved 4, 5, 6; + + // Whether the message is an automatically generated map entry type for the + // maps field. + // + // For maps fields: + // map map_field = 1; + // The parsed descriptor looks like: + // message MapFieldEntry { + // option map_entry = true; + // optional KeyType key = 1; + // optional ValueType value = 2; + // } + // repeated MapFieldEntry map_field = 1; + // + // Implementations may choose not to generate the map_entry=true message, but + // use a native map in the target language to hold the keys and values. + // The reflection APIs in such implementations still need to work as + // if the field is a repeated message field. + // + // NOTE: Do not set the option in .proto files. Always use the maps syntax + // instead. The option should only be implicitly set by the proto compiler + // parser. + optional bool map_entry = 7; + + reserved 8; // javalite_serializable + reserved 9; // javanano_as_lite + + // Enable the legacy handling of JSON field name conflicts. This lowercases + // and strips underscored from the fields before comparison in proto3 only. + // The new behavior takes `json_name` into account and applies to proto2 as + // well. + // + // This should only be used as a temporary measure against broken builds due + // to the change in behavior for JSON field name conflicts. + // + // TODO This is legacy behavior we plan to remove once downstream + // teams have had time to migrate. + optional bool deprecated_legacy_json_field_conflicts = 11 [deprecated = true]; + + // Any features defined in the specific edition. + // WARNING: This field should only be used by protobuf plugins or special + // cases like the proto compiler. Other uses are discouraged and + // developers should rely on the protoreflect APIs for their client language. + optional FeatureSet features = 12; + + // Range reserved for first-class custom options defined by the Protobuf + // team. User custom options must use the 1000+ range instead. + extensions 990 to 998; + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +message FieldOptions { + // NOTE: ctype is deprecated. Use `features.(pb.cpp).string_type` instead. + // The ctype option instructs the C++ code generator to use a different + // representation of the field than it normally would. See the specific + // options below. This option is only implemented to support use of + // [ctype=CORD] and [ctype=STRING] (the default) on non-repeated fields of + // type "bytes" in the open source release. + // TODO: make ctype actually deprecated. + optional CType ctype = 1 [/*deprecated = true,*/ default = STRING]; + enum CType { + // Default mode. + STRING = 0; + + // The option [ctype=CORD] may be applied to a non-repeated field of type + // "bytes". It indicates that in C++, the data should be stored in a Cord + // instead of a string. For very large strings, this may reduce memory + // fragmentation. It may also allow better performance when parsing from a + // Cord, or when parsing with aliasing enabled, as the parsed Cord may then + // alias the original buffer. + CORD = 1; + + STRING_PIECE = 2; + } + // The packed option can be enabled for repeated primitive fields to enable + // a more efficient representation on the wire. Rather than repeatedly + // writing the tag and type for each element, the entire array is encoded as + // a single length-delimited blob. In proto3, only explicit setting it to + // false will avoid using packed encoding. This option is prohibited in + // Editions, but the `repeated_field_encoding` feature can be used to control + // the behavior. + optional bool packed = 2; + + // The jstype option determines the JavaScript type used for values of the + // field. The option is permitted only for 64 bit integral and fixed types + // (int64, uint64, sint64, fixed64, sfixed64). A field with jstype JS_STRING + // is represented as JavaScript string, which avoids loss of precision that + // can happen when a large value is converted to a floating point JavaScript. + // Specifying JS_NUMBER for the jstype causes the generated JavaScript code to + // use the JavaScript "number" type. The behavior of the default option + // JS_NORMAL is implementation dependent. + // + // This option is an enum to permit additional types to be added, e.g. + // goog.math.Integer. + optional JSType jstype = 6 [default = JS_NORMAL]; + enum JSType { + // Use the default type. + JS_NORMAL = 0; + + // Use JavaScript strings. + JS_STRING = 1; + + // Use JavaScript numbers. + JS_NUMBER = 2; + } + + // Should this field be parsed lazily? Lazy applies only to message-type + // fields. It means that when the outer message is initially parsed, the + // inner message's contents will not be parsed but instead stored in encoded + // form. The inner message will actually be parsed when it is first accessed. + // + // This is only a hint. Implementations are free to choose whether to use + // eager or lazy parsing regardless of the value of this option. However, + // setting this option true suggests that the protocol author believes that + // using lazy parsing on this field is worth the additional bookkeeping + // overhead typically needed to implement it. + // + // This option does not affect the public interface of any generated code; + // all method signatures remain the same. Furthermore, thread-safety of the + // interface is not affected by this option; const methods remain safe to + // call from multiple threads concurrently, while non-const methods continue + // to require exclusive access. + // + // Note that lazy message fields are still eagerly verified to check + // ill-formed wireformat or missing required fields. Calling IsInitialized() + // on the outer message would fail if the inner message has missing required + // fields. Failed verification would result in parsing failure (except when + // uninitialized messages are acceptable). + optional bool lazy = 5 [default = false]; + + // unverified_lazy does no correctness checks on the byte stream. This should + // only be used where lazy with verification is prohibitive for performance + // reasons. + optional bool unverified_lazy = 15 [default = false]; + + // Is this field deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for accessors, or it will be completely ignored; in the very least, this + // is a formalization for deprecating fields. + optional bool deprecated = 3 [default = false]; + + // DEPRECATED. DO NOT USE! + // For Google-internal migration only. Do not use. + optional bool weak = 10 [default = false, deprecated = true]; + + // Indicate that the field value should not be printed out when using debug + // formats, e.g. when the field contains sensitive credentials. + optional bool debug_redact = 16 [default = false]; + + // If set to RETENTION_SOURCE, the option will be omitted from the binary. + enum OptionRetention { + RETENTION_UNKNOWN = 0; + RETENTION_RUNTIME = 1; + RETENTION_SOURCE = 2; + } + + optional OptionRetention retention = 17; + + // This indicates the types of entities that the field may apply to when used + // as an option. If it is unset, then the field may be freely used as an + // option on any kind of entity. + enum OptionTargetType { + TARGET_TYPE_UNKNOWN = 0; + TARGET_TYPE_FILE = 1; + TARGET_TYPE_EXTENSION_RANGE = 2; + TARGET_TYPE_MESSAGE = 3; + TARGET_TYPE_FIELD = 4; + TARGET_TYPE_ONEOF = 5; + TARGET_TYPE_ENUM = 6; + TARGET_TYPE_ENUM_ENTRY = 7; + TARGET_TYPE_SERVICE = 8; + TARGET_TYPE_METHOD = 9; + } + + repeated OptionTargetType targets = 19; + + message EditionDefault { + optional Edition edition = 3; + optional string value = 2; // Textproto value. + } + repeated EditionDefault edition_defaults = 20; + + // Any features defined in the specific edition. + // WARNING: This field should only be used by protobuf plugins or special + // cases like the proto compiler. Other uses are discouraged and + // developers should rely on the protoreflect APIs for their client language. + optional FeatureSet features = 21; + + // Information about the support window of a feature. + message FeatureSupport { + // The edition that this feature was first available in. In editions + // earlier than this one, the default assigned to EDITION_LEGACY will be + // used, and proto files will not be able to override it. + optional Edition edition_introduced = 1; + + // The edition this feature becomes deprecated in. Using this after this + // edition may trigger warnings. + optional Edition edition_deprecated = 2; + + // The deprecation warning text if this feature is used after the edition it + // was marked deprecated in. + optional string deprecation_warning = 3; + + // The edition this feature is no longer available in. In editions after + // this one, the last default assigned will be used, and proto files will + // not be able to override it. + optional Edition edition_removed = 4; + + // The removal error text if this feature is used after the edition it was + // removed in. + optional string removal_error = 5; + } + optional FeatureSupport feature_support = 22; + + // Range reserved for first-class custom options defined by the Protobuf + // team. User custom options must use the 1000+ range instead. + extensions 990 to 998; + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; + + reserved 4; // removed jtype + reserved 18; // reserve target, target_obsolete_do_not_use +} + +message OneofOptions { + // Any features defined in the specific edition. + // WARNING: This field should only be used by protobuf plugins or special + // cases like the proto compiler. Other uses are discouraged and + // developers should rely on the protoreflect APIs for their client language. + optional FeatureSet features = 1; + + // Range reserved for first-class custom options defined by the Protobuf + // team. User custom options must use the 1000+ range instead. + extensions 990 to 998; + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +message EnumOptions { + + // Set this option to true to allow mapping different tag names to the same + // value. + optional bool allow_alias = 2; + + // Is this enum deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for the enum, or it will be completely ignored; in the very least, this + // is a formalization for deprecating enums. + optional bool deprecated = 3 [default = false]; + + reserved 5; // javanano_as_lite + + // Enable the legacy handling of JSON field name conflicts. This lowercases + // and strips underscored from the fields before comparison in proto3 only. + // The new behavior takes `json_name` into account and applies to proto2 as + // well. + // TODO Remove this legacy behavior once downstream teams have + // had time to migrate. + optional bool deprecated_legacy_json_field_conflicts = 6 [deprecated = true]; + + // Any features defined in the specific edition. + // WARNING: This field should only be used by protobuf plugins or special + // cases like the proto compiler. Other uses are discouraged and + // developers should rely on the protoreflect APIs for their client language. + optional FeatureSet features = 7; + + // Range reserved for first-class custom options defined by the Protobuf + // team. User custom options must use the 1000+ range instead. + extensions 990 to 998; + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +message EnumValueOptions { + // Is this enum value deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for the enum value, or it will be completely ignored; in the very least, + // this is a formalization for deprecating enum values. + optional bool deprecated = 1 [default = false]; + + // Any features defined in the specific edition. + // WARNING: This field should only be used by protobuf plugins or special + // cases like the proto compiler. Other uses are discouraged and + // developers should rely on the protoreflect APIs for their client language. + optional FeatureSet features = 2; + + // Indicate that fields annotated with this enum value should not be printed + // out when using debug formats, e.g. when the field contains sensitive + // credentials. + optional bool debug_redact = 3 [default = false]; + + // Information about the support window of a feature value. + optional FieldOptions.FeatureSupport feature_support = 4; + + // Range reserved for first-class extension options defined by the Protobuf + // team. Custom options must use the 1000+ range instead. + extensions 990 to 998 [declaration = { + number: 998, + full_name: ".pb.enumvalue.json", + type: ".pb.enumvalue.JsonEnumValueOptions" + }]; + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +message ServiceOptions { + + // Any features defined in the specific edition. + // WARNING: This field should only be used by protobuf plugins or special + // cases like the proto compiler. Other uses are discouraged and + // developers should rely on the protoreflect APIs for their client language. + optional FeatureSet features = 34; + + // Note: Field numbers 1 through 32 are reserved for Google's internal RPC + // framework. We apologize for hoarding these numbers to ourselves, but + // we were already using them long before we decided to release Protocol + // Buffers. + + // Is this service deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for the service, or it will be completely ignored; in the very least, + // this is a formalization for deprecating services. + optional bool deprecated = 33 [default = false]; + + // Range reserved for first-class custom options defined by the Protobuf + // team. User custom options must use the 1000+ range instead. + extensions 990 to 998; + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +message MethodOptions { + + // Note: Field numbers 1 through 32 are reserved for Google's internal RPC + // framework. We apologize for hoarding these numbers to ourselves, but + // we were already using them long before we decided to release Protocol + // Buffers. + + // Is this method deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for the method, or it will be completely ignored; in the very least, + // this is a formalization for deprecating methods. + optional bool deprecated = 33 [default = false]; + + // Is this method side-effect-free (or safe in HTTP parlance), or idempotent, + // or neither? HTTP based RPC implementation may choose GET verb for safe + // methods, and PUT verb for idempotent methods instead of the default POST. + enum IdempotencyLevel { + IDEMPOTENCY_UNKNOWN = 0; + NO_SIDE_EFFECTS = 1; // implies idempotent + IDEMPOTENT = 2; // idempotent, but may have side effects + } + optional IdempotencyLevel idempotency_level = 34 + [default = IDEMPOTENCY_UNKNOWN]; + + // Any features defined in the specific edition. + // WARNING: This field should only be used by protobuf plugins or special + // cases like the proto compiler. Other uses are discouraged and + // developers should rely on the protoreflect APIs for their client language. + optional FeatureSet features = 35; + + // Range reserved for first-class custom options defined by the Protobuf + // team. User custom options must use the 1000+ range instead. + extensions 990 to 998; + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +// A message representing a option the parser does not recognize. This only +// appears in options protos created by the compiler::Parser class. +// DescriptorPool resolves these when building Descriptor objects. Therefore, +// options protos in descriptor objects (e.g. returned by Descriptor::options(), +// or produced by Descriptor::CopyTo()) will never have UninterpretedOptions +// in them. +message UninterpretedOption { + // The name of the uninterpreted option. Each string represents a segment in + // a dot-separated name. is_extension is true iff a segment represents an + // extension (denoted with parentheses in options specs in .proto files). + // E.g.,{ ["foo", false], ["bar.baz", true], ["moo", false] } represents + // "foo.(bar.baz).moo". + message NamePart { + required string name_part = 1; + required bool is_extension = 2; + } + repeated NamePart name = 2; + + // The value of the uninterpreted option, in whatever type the tokenizer + // identified it as during parsing. Exactly one of these should be set. + optional string identifier_value = 3; + optional uint64 positive_int_value = 4; + optional int64 negative_int_value = 5; + optional double double_value = 6; + optional bytes string_value = 7; + optional string aggregate_value = 8; +} + +// =================================================================== +// Features + +// TODO Enums in C++ gencode (and potentially other languages) are +// not well scoped. This means that each of the feature enums below can clash +// with each other. The short names we've chosen maximize call-site +// readability, but leave us very open to this scenario. A future feature will +// be designed and implemented to handle this, hopefully before we ever hit a +// conflict here. +message FeatureSet { + enum FieldPresence { + FIELD_PRESENCE_UNKNOWN = 0; + EXPLICIT = 1; + IMPLICIT = 2; + LEGACY_REQUIRED = 3; + } + optional FieldPresence field_presence = 1 [ + retention = RETENTION_RUNTIME, + targets = TARGET_TYPE_FIELD, + targets = TARGET_TYPE_FILE, + feature_support = { + edition_introduced: EDITION_2023, + }, + edition_defaults = { edition: EDITION_LEGACY, value: "EXPLICIT" }, + edition_defaults = { edition: EDITION_PROTO3, value: "IMPLICIT" }, + edition_defaults = { edition: EDITION_2023, value: "EXPLICIT" } + ]; + + enum EnumType { + ENUM_TYPE_UNKNOWN = 0; + OPEN = 1; + CLOSED = 2; + } + optional EnumType enum_type = 2 [ + retention = RETENTION_RUNTIME, + targets = TARGET_TYPE_ENUM, + targets = TARGET_TYPE_FILE, + feature_support = { + edition_introduced: EDITION_2023, + }, + edition_defaults = { edition: EDITION_LEGACY, value: "CLOSED" }, + edition_defaults = { edition: EDITION_PROTO3, value: "OPEN" } + ]; + + enum RepeatedFieldEncoding { + REPEATED_FIELD_ENCODING_UNKNOWN = 0; + PACKED = 1; + EXPANDED = 2; + } + optional RepeatedFieldEncoding repeated_field_encoding = 3 [ + retention = RETENTION_RUNTIME, + targets = TARGET_TYPE_FIELD, + targets = TARGET_TYPE_FILE, + feature_support = { + edition_introduced: EDITION_2023, + }, + edition_defaults = { edition: EDITION_LEGACY, value: "EXPANDED" }, + edition_defaults = { edition: EDITION_PROTO3, value: "PACKED" } + ]; + + enum Utf8Validation { + UTF8_VALIDATION_UNKNOWN = 0; + VERIFY = 2; + NONE = 3; + reserved 1; + } + optional Utf8Validation utf8_validation = 4 [ + retention = RETENTION_RUNTIME, + targets = TARGET_TYPE_FIELD, + targets = TARGET_TYPE_FILE, + feature_support = { + edition_introduced: EDITION_2023, + }, + edition_defaults = { edition: EDITION_LEGACY, value: "NONE" }, + edition_defaults = { edition: EDITION_PROTO3, value: "VERIFY" } + ]; + + enum MessageEncoding { + MESSAGE_ENCODING_UNKNOWN = 0; + LENGTH_PREFIXED = 1; + DELIMITED = 2; + } + optional MessageEncoding message_encoding = 5 [ + retention = RETENTION_RUNTIME, + targets = TARGET_TYPE_FIELD, + targets = TARGET_TYPE_FILE, + feature_support = { + edition_introduced: EDITION_2023, + }, + edition_defaults = { edition: EDITION_LEGACY, value: "LENGTH_PREFIXED" } + ]; + + enum JsonFormat { + JSON_FORMAT_UNKNOWN = 0; + ALLOW = 1; + LEGACY_BEST_EFFORT = 2; + } + optional JsonFormat json_format = 6 [ + retention = RETENTION_RUNTIME, + targets = TARGET_TYPE_MESSAGE, + targets = TARGET_TYPE_ENUM, + targets = TARGET_TYPE_FILE, + feature_support = { + edition_introduced: EDITION_2023, + }, + edition_defaults = { edition: EDITION_LEGACY, value: "LEGACY_BEST_EFFORT" }, + edition_defaults = { edition: EDITION_PROTO3, value: "ALLOW" } + ]; + + enum EnforceNamingStyle { + ENFORCE_NAMING_STYLE_UNKNOWN = 0; + STYLE2024 = 1; + STYLE_LEGACY = 2; + STYLE2026 = 3; + } + optional EnforceNamingStyle enforce_naming_style = 7 [ + retention = RETENTION_SOURCE, + targets = TARGET_TYPE_FILE, + targets = TARGET_TYPE_EXTENSION_RANGE, + targets = TARGET_TYPE_MESSAGE, + targets = TARGET_TYPE_FIELD, + targets = TARGET_TYPE_ONEOF, + targets = TARGET_TYPE_ENUM, + targets = TARGET_TYPE_ENUM_ENTRY, + targets = TARGET_TYPE_SERVICE, + targets = TARGET_TYPE_METHOD, + feature_support = { + edition_introduced: EDITION_2024, + }, + edition_defaults = { edition: EDITION_LEGACY, value: "STYLE_LEGACY" }, + edition_defaults = { edition: EDITION_2024, value: "STYLE2024" }, + edition_defaults = { edition: EDITION_2026, value: "STYLE2026" } + ]; + + message VisibilityFeature { + enum DefaultSymbolVisibility { + DEFAULT_SYMBOL_VISIBILITY_UNKNOWN = 0; + + // Default pre-EDITION_2024, all UNSET visibility are export. + EXPORT_ALL = 1; + + // All top-level symbols default to export, nested default to local. + EXPORT_TOP_LEVEL = 2; + + // All symbols default to local. + LOCAL_ALL = 3; + + // All symbols local by default. Nested types cannot be exported. + // With special case caveat for message { enum {} reserved 1 to max; } + // This is the recommended setting for new protos. + STRICT = 4; + } + reserved 1 to max; + } + optional VisibilityFeature.DefaultSymbolVisibility default_symbol_visibility = + 8 [ + retention = RETENTION_SOURCE, + targets = TARGET_TYPE_FILE, + feature_support = { + edition_introduced: EDITION_2024, + }, + edition_defaults = { edition: EDITION_LEGACY, value: "EXPORT_ALL" }, + edition_defaults = { edition: EDITION_2024, value: "EXPORT_TOP_LEVEL" } + ]; + + reserved 999; + + extensions 1000 to 9994 [ + declaration = { + number: 1000, + full_name: ".pb.cpp", + type: ".pb.CppFeatures" + }, + declaration = { + number: 1001, + full_name: ".pb.java", + type: ".pb.JavaFeatures" + }, + declaration = { number: 1002, full_name: ".pb.go", type: ".pb.GoFeatures" }, + declaration = { + number: 1003, + full_name: ".pb.python", + type: ".pb.PythonFeatures" + }, + declaration = { + number: 1004, + full_name: ".pb.csharp", + type: ".pb.CSharpFeatures" + }, + declaration = { + number: 1100, + full_name: ".imp.impress_feature_set", + type: ".imp.ImpressFeatureSet" + }, + declaration = { + number: 9989, + full_name: ".pb.java_mutable", + type: ".pb.JavaMutableFeatures" + }, + declaration = { + number: 9990, + full_name: ".pb.proto1", + type: ".pb.Proto1Features" + } + ]; + + extensions 9995 to 9999; // For internal testing + extensions 10000; // for https://github.com/bufbuild/protobuf-es +} + +// A compiled specification for the defaults of a set of features. These +// messages are generated from FeatureSet extensions and can be used to seed +// feature resolution. The resolution with this object becomes a simple search +// for the closest matching edition, followed by proto merges. +message FeatureSetDefaults { + // A map from every known edition with a unique set of defaults to its + // defaults. Not all editions may be contained here. For a given edition, + // the defaults at the closest matching edition ordered at or before it should + // be used. This field must be in strict ascending order by edition. + message FeatureSetEditionDefault { + optional Edition edition = 3; + + // Defaults of features that can be overridden in this edition. + optional FeatureSet overridable_features = 4; + + // Defaults of features that can't be overridden in this edition. + optional FeatureSet fixed_features = 5; + + reserved 1, 2; + reserved "features"; + } + repeated FeatureSetEditionDefault defaults = 1; + + // The minimum supported edition (inclusive) when this was constructed. + // Editions before this will not have defaults. + optional Edition minimum_edition = 4; + + // The maximum known edition (inclusive) when this was constructed. Editions + // after this will not have reliable defaults. + optional Edition maximum_edition = 5; +} + +// =================================================================== +// Optional source code info + +// Encapsulates information about the original source file from which a +// FileDescriptorProto was generated. +message SourceCodeInfo { + // A Location identifies a piece of source code in a .proto file which + // corresponds to a particular definition. This information is intended + // to be useful to IDEs, code indexers, documentation generators, and similar + // tools. + // + // For example, say we have a file like: + // message Foo { + // optional string foo = 1; + // } + // Let's look at just the field definition: + // optional string foo = 1; + // ^ ^^ ^^ ^ ^^^ + // a bc de f ghi + // We have the following locations: + // span path represents + // [a,i) [ 4, 0, 2, 0 ] The whole field definition. + // [a,b) [ 4, 0, 2, 0, 4 ] The label (optional). + // [c,d) [ 4, 0, 2, 0, 5 ] The type (string). + // [e,f) [ 4, 0, 2, 0, 1 ] The name (foo). + // [g,h) [ 4, 0, 2, 0, 3 ] The number (1). + // + // Notes: + // - A location may refer to a repeated field itself (i.e. not to any + // particular index within it). This is used whenever a set of elements are + // logically enclosed in a single code segment. For example, an entire + // extend block (possibly containing multiple extension definitions) will + // have an outer location whose path refers to the "extensions" repeated + // field without an index. + // - Multiple locations may have the same path. This happens when a single + // logical declaration is spread out across multiple places. The most + // obvious example is the "extend" block again -- there may be multiple + // extend blocks in the same scope, each of which will have the same path. + // - A location's span is not always a subset of its parent's span. For + // example, the "extendee" of an extension declaration appears at the + // beginning of the "extend" block and is shared by all extensions within + // the block. + // - Just because a location's span is a subset of some other location's span + // does not mean that it is a descendant. For example, a "group" defines + // both a type and a field in a single declaration. Thus, the locations + // corresponding to the type and field and their components will overlap. + // - Code which tries to interpret locations should probably be designed to + // ignore those that it doesn't understand, as more types of locations could + // be recorded in the future. + repeated Location location = 1; + message Location { + // Identifies which part of the FileDescriptorProto was defined at this + // location. + // + // Each element is a field number or an index. They form a path from + // the root FileDescriptorProto to the place where the definition appears. + // For example, this path: + // [ 4, 3, 2, 7, 1 ] + // refers to: + // file.message_type(3) // 4, 3 + // .field(7) // 2, 7 + // .name() // 1 + // This is because FileDescriptorProto.message_type has field number 4: + // repeated DescriptorProto message_type = 4; + // and DescriptorProto.field has field number 2: + // repeated FieldDescriptorProto field = 2; + // and FieldDescriptorProto.name has field number 1: + // optional string name = 1; + // + // Thus, the above path gives the location of a field name. If we removed + // the last element: + // [ 4, 3, 2, 7 ] + // this path refers to the whole field declaration (from the beginning + // of the label to the terminating semicolon). + repeated int32 path = 1 [packed = true]; + + // Always has exactly three or four elements: start line, start column, + // end line (optional, otherwise assumed same as start line), end column. + // These are packed into a single field for efficiency. Note that line + // and column numbers are zero-based -- typically you will want to add + // 1 to each before displaying to a user. + repeated int32 span = 2 [packed = true]; + + // If this SourceCodeInfo represents a complete declaration, these are any + // comments appearing before and after the declaration which appear to be + // attached to the declaration. + // + // A series of line comments appearing on consecutive lines, with no other + // tokens appearing on those lines, will be treated as a single comment. + // + // leading_detached_comments will keep paragraphs of comments that appear + // before (but not connected to) the current element. Each paragraph, + // separated by empty lines, will be one comment element in the repeated + // field. + // + // Only the comment content is provided; comment markers (e.g. //) are + // stripped out. For block comments, leading whitespace and an asterisk + // will be stripped from the beginning of each line other than the first. + // Newlines are included in the output. + // + // Examples: + // + // optional int32 foo = 1; // Comment attached to foo. + // // Comment attached to bar. + // optional int32 bar = 2; + // + // optional string baz = 3; + // // Comment attached to baz. + // // Another line attached to baz. + // + // // Comment attached to moo. + // // + // // Another line attached to moo. + // optional double moo = 4; + // + // // Detached comment for corge. This is not leading or trailing comments + // // to moo or corge because there are blank lines separating it from + // // both. + // + // // Detached comment for corge paragraph 2. + // + // optional string corge = 5; + // /* Block comment attached + // * to corge. Leading asterisks + // * will be removed. */ + // /* Block comment attached to + // * grault. */ + // optional int32 grault = 6; + // + // // ignored detached comments. + optional string leading_comments = 3; + optional string trailing_comments = 4; + repeated string leading_detached_comments = 6; + } + + // Extensions for tooling. + extensions 536000000 [declaration = { + number: 536000000 + type: ".buf.descriptor.v1.SourceCodeInfoExtension" + full_name: ".buf.descriptor.v1.buf_source_code_info_extension" + }]; +} + +// Describes the relationship between generated code and its original source +// file. A GeneratedCodeInfo message is associated with only one generated +// source file, but may contain references to different source .proto files. +message GeneratedCodeInfo { + // An Annotation connects some span of text in generated code to an element + // of its generating .proto file. + repeated Annotation annotation = 1; + message Annotation { + // Identifies the element in the original source .proto file. This field + // is formatted the same as SourceCodeInfo.Location.path. + repeated int32 path = 1 [packed = true]; + + // Identifies the filesystem path to the original source .proto. + optional string source_file = 2; + + // Identifies the starting offset in bytes in the generated code + // that relates to the identified object. + optional int32 begin = 3; + + // Identifies the ending offset in bytes in the generated code that + // relates to the identified object. The end offset should be one past + // the last relevant byte (so the length of the text = end - begin). + optional int32 end = 4; + + // Represents the identified object's effect on the element in the original + // .proto file. + enum Semantic { + // There is no effect or the effect is indescribable. + NONE = 0; + // The element is set or otherwise mutated. + SET = 1; + // An alias to the element is returned. + ALIAS = 2; + } + optional Semantic semantic = 5; + } +} + +// Describes the 'visibility' of a symbol with respect to the proto import +// system. Symbols can only be imported when the visibility rules do not prevent +// it (ex: local symbols cannot be imported). Visibility modifiers can only set +// on `message` and `enum` as they are the only types available to be referenced +// from other files. +enum SymbolVisibility { + VISIBILITY_UNSET = 0; + VISIBILITY_LOCAL = 1; + VISIBILITY_EXPORT = 2; +} diff --git a/buildkit-proto/proto/google/rpc/status.proto b/buildkit-proto/proto/google/rpc/status.proto new file mode 100644 index 0000000..97f50b9 --- /dev/null +++ b/buildkit-proto/proto/google/rpc/status.proto @@ -0,0 +1,48 @@ +// Copyright 2026 Google LLC +// +// 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. + +syntax = "proto3"; + +package google.rpc; + +import "google/protobuf/any.proto"; + +option go_package = "google.golang.org/genproto/googleapis/rpc/status;status"; +option java_multiple_files = true; +option java_outer_classname = "StatusProto"; +option java_package = "com.google.rpc"; +option objc_class_prefix = "RPC"; + +// The `Status` type defines a logical error model that is suitable for +// different programming environments, including REST APIs and RPC APIs. It is +// used by [gRPC](https://github.com/grpc). Each `Status` message contains +// three pieces of data: error code, error message, and error details. +// +// You can find out more about this error model and how to work with it in the +// [API Design Guide](https://cloud.google.com/apis/design/errors). +message Status { + // The status code, which should be an enum value of + // [google.rpc.Code][google.rpc.Code]. + int32 code = 1; + + // A developer-facing error message, which should be in English. Any + // user-facing error message should be localized and sent in the + // [google.rpc.Status.details][google.rpc.Status.details] field, or localized + // by the client. + string message = 2; + + // A list of messages that carry the error details. There is a common set of + // message types for APIs to use. + repeated google.protobuf.Any details = 3; +} diff --git a/buildkit-proto/update.sh b/buildkit-proto/update.sh index 229c39a..4b36e00 100755 --- a/buildkit-proto/update.sh +++ b/buildkit-proto/update.sh @@ -1,7 +1,7 @@ #!/bin/sh set -e -export BUILDKIT_VERSION="v0.18" +export BUILDKIT_VERSION="v0.29.0" # Create all required directories mkdir -p proto/github.com/moby/buildkit/api/types From bef8d8b43b487b3185846bf01e0ec904de28dce4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 26 Apr 2026 16:31:08 +0000 Subject: [PATCH 02/21] buildkit-llb: align struct literals with v0.29 proto schema Adds the new ExecOp.cdi_devices field to the production code path in ops/exec/command.rs. Test fixtures in ops/exec/mod.rs and ops/fs/mod.rs were already missing several fields added in earlier proto upgrades (Mount.tmpfs_opt / result_id / content_cache, Meta.hostname / cgroup_parent / ulimit / remove_mount_stubs_recursive / valid_exit_codes, FileActionCopy.required_paths and friends). They are now closed with `..Default::default()` so future proto schema additions don't keep breaking the same fixtures. `cargo build -p buildkit-llb` succeeds. https://claude.ai/code/session_01XtZHcL6rKJDuX7tUS3okdc --- buildkit-llb/src/ops/exec/command.rs | 1 + buildkit-llb/src/ops/exec/mod.rs | 44 +++++++++++++++------------- buildkit-llb/src/ops/fs/mod.rs | 7 +++++ 3 files changed, 32 insertions(+), 20 deletions(-) diff --git a/buildkit-llb/src/ops/exec/command.rs b/buildkit-llb/src/ops/exec/command.rs index 820a19e..c262b59 100644 --- a/buildkit-llb/src/ops/exec/command.rs +++ b/buildkit-llb/src/ops/exec/command.rs @@ -300,6 +300,7 @@ impl<'a> Operation for Command<'a> { security: SecurityMode::Sandbox.into(), meta: Some(self.context.clone().into()), secretenv: Vec::new(), + cdi_devices: Vec::new(), })), inputs: inputs.into_iter().flatten().collect(), diff --git a/buildkit-llb/src/ops/exec/mod.rs b/buildkit-llb/src/ops/exec/mod.rs index 8b03659..4db1bd7 100644 --- a/buildkit-llb/src/ops/exec/mod.rs +++ b/buildkit-llb/src/ops/exec/mod.rs @@ -38,10 +38,9 @@ fn serialization() { cwd: "/".into(), user: "root".into(), - extra_hosts: vec![], - proxy_env: None, + ..Default::default() }), - secretenv: vec![], + ..Default::default() }) }, ); @@ -81,9 +80,9 @@ fn serialization_with_env_iter() { cwd: "/".into(), user: "root".into(), - extra_hosts: vec![], - proxy_env: None, + ..Default::default() }), + ..Default::default() }) }, ); @@ -108,13 +107,12 @@ fn serialization_with_cwd() { security: SecurityMode::Sandbox.into(), meta: Some(Meta { args: crate::utils::test::to_vec(vec!["cargo", "build"]), - env: vec![], cwd: "/rust-src".into(), user: "root".into(), - extra_hosts: vec![], - proxy_env: None, + ..Default::default() }), + ..Default::default() }) }, ); @@ -139,13 +137,12 @@ fn serialization_with_user() { security: SecurityMode::Sandbox.into(), meta: Some(Meta { args: crate::utils::test::to_vec(vec!["cargo", "build"]), - env: vec![], cwd: "/".into(), user: "builder".into(), - extra_hosts: vec![], - proxy_env: None, + ..Default::default() }), + ..Default::default() }) }, ); @@ -222,6 +219,7 @@ fn serialization_with_mounts() { cache_opt: None, secret_opt: None, ssh_opt: None, + ..Default::default() }, pb::Mount { input: -1, @@ -233,6 +231,7 @@ fn serialization_with_mounts() { cache_opt: None, secret_opt: None, ssh_opt: None, + ..Default::default() }, pb::Mount { input: 1, @@ -244,6 +243,7 @@ fn serialization_with_mounts() { cache_opt: None, secret_opt: None, ssh_opt: None, + ..Default::default() }, pb::Mount { input: 2, @@ -255,6 +255,7 @@ fn serialization_with_mounts() { cache_opt: None, secret_opt: None, ssh_opt: None, + ..Default::default() }, pb::Mount { input: -1, @@ -269,19 +270,19 @@ fn serialization_with_mounts() { }), secret_opt: None, ssh_opt: None, + ..Default::default() }, ], network: NetMode::Unset.into(), security: SecurityMode::Sandbox.into(), meta: Some(Meta { args: crate::utils::test::to_vec(vec!["cargo", "build"]), - env: vec![], cwd: "/".into(), user: "root".into(), - extra_hosts: vec![], - proxy_env: None, + ..Default::default() }), + ..Default::default() }) }, ); @@ -329,6 +330,7 @@ fn serialization_with_several_root_mounts() { cache_opt: None, secret_opt: None, ssh_opt: None, + ..Default::default() }, pb::Mount { input: -1, @@ -340,6 +342,7 @@ fn serialization_with_several_root_mounts() { cache_opt: None, secret_opt: None, ssh_opt: None, + ..Default::default() }, pb::Mount { input: -1, @@ -351,19 +354,19 @@ fn serialization_with_several_root_mounts() { cache_opt: None, secret_opt: None, ssh_opt: None, + ..Default::default() }, ], network: NetMode::Unset.into(), security: SecurityMode::Sandbox.into(), meta: Some(Meta { args: crate::utils::test::to_vec(vec!["cargo", "build"]), - env: vec![], cwd: "/".into(), user: "root".into(), - extra_hosts: vec![], - proxy_env: None, + ..Default::default() }), + ..Default::default() }) }, ); @@ -407,6 +410,7 @@ fn serialization_with_ssh_mounts() { cache_opt: None, secret_opt: None, ssh_opt: None, + ..Default::default() }, pb::Mount { input: -1, @@ -422,19 +426,19 @@ fn serialization_with_ssh_mounts() { optional: true, ..Default::default() }), + ..Default::default() }, ], network: NetMode::Unset.into(), security: SecurityMode::Sandbox.into(), meta: Some(Meta { args: crate::utils::test::to_vec(vec!["cargo", "build"]), - env: vec![], cwd: "/".into(), user: "root".into(), - extra_hosts: vec![], - proxy_env: None, + ..Default::default() }), + ..Default::default() }) }, ); diff --git a/buildkit-llb/src/ops/fs/mod.rs b/buildkit-llb/src/ops/fs/mod.rs index d785303..ddf66db 100644 --- a/buildkit-llb/src/ops/fs/mod.rs +++ b/buildkit-llb/src/ops/fs/mod.rs @@ -121,6 +121,7 @@ fn copy_serialization() { allow_wildcard: false, allow_empty_wildcard: false, timestamp: -1, + ..Default::default() })), }, FileAction { @@ -139,6 +140,7 @@ fn copy_serialization() { allow_wildcard: false, allow_empty_wildcard: false, timestamp: -1, + ..Default::default() })), }, FileAction { @@ -157,6 +159,7 @@ fn copy_serialization() { allow_wildcard: false, allow_empty_wildcard: false, timestamp: -1, + ..Default::default() })), }, ], @@ -246,6 +249,7 @@ fn copy_with_params_serialization() { allow_wildcard: false, allow_empty_wildcard: false, timestamp: -1, + ..Default::default() })), }, FileAction { @@ -264,6 +268,7 @@ fn copy_with_params_serialization() { allow_wildcard: false, allow_empty_wildcard: false, timestamp: -1, + ..Default::default() })), }, FileAction { @@ -282,6 +287,7 @@ fn copy_with_params_serialization() { allow_wildcard: false, allow_empty_wildcard: false, timestamp: -1, + ..Default::default() })), }, FileAction { @@ -300,6 +306,7 @@ fn copy_with_params_serialization() { allow_wildcard: true, allow_empty_wildcard: false, timestamp: -1, + ..Default::default() })), }, ], From 32cffe65dab4d3ae482d916c7e1c6bcb636a067a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 26 Apr 2026 16:31:47 +0000 Subject: [PATCH 03/21] buildkit-frontend: fill in new gateway proto fields on requests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ResolveImageConfigRequest gained resolver_type, session_id, store_id and source_policies in v0.18+ — keep existing fields and let prost defaults cover the rest via `..Default::default()`. - ReadFileRequest gained mount_index in v0.29. - frontend Result gained attestations in v0.18+. Using `..Default::default()` keeps these literals robust against future schema additions. https://claude.ai/code/session_01XtZHcL6rKJDuX7tUS3okdc --- buildkit-frontend/src/bridge.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/buildkit-frontend/src/bridge.rs b/buildkit-frontend/src/bridge.rs index 6b098a6..3152289 100644 --- a/buildkit-frontend/src/bridge.rs +++ b/buildkit-frontend/src/bridge.rs @@ -47,6 +47,8 @@ impl Bridge { platform: None, resolve_mode: image.resolve_mode().unwrap_or_default().to_string(), log_name: log.unwrap_or_default().into(), + + ..Default::default() }; debug!("requesting to resolve an image: {:?}", request); @@ -130,6 +132,8 @@ impl Bridge { r#ref: layer.0.clone(), file_path, range, + + ..Default::default() }; let response = { @@ -162,6 +166,8 @@ impl Bridge { result: Some(Output { result: Some(RefResult::Ref(output.0)), metadata, + + ..Default::default() }), }; From 077c6595dc2bb490625eeeb0cab82eb04c7c022c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 26 Apr 2026 16:32:30 +0000 Subject: [PATCH 04/21] buildkit-frontend: adapt RefResult::Ref to new Ref message type The frontend Result.result oneof variant `ref = 3` was promoted from a plain string to a `Ref { id, def }` message. Update the solve response match (extract `id`) and finish_with_success construction (wrap output.0 in `Ref { id, def: None }`). https://claude.ai/code/session_01XtZHcL6rKJDuX7tUS3okdc --- buildkit-frontend/src/bridge.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/buildkit-frontend/src/bridge.rs b/buildkit-frontend/src/bridge.rs index 3152289..2782eed 100644 --- a/buildkit-frontend/src/bridge.rs +++ b/buildkit-frontend/src/bridge.rs @@ -12,8 +12,8 @@ use tonic::Request; use buildkit_proto::google::rpc::Status; use buildkit_proto::moby::buildkit::v1::frontend::llb_bridge_client::LlbBridgeClient; use buildkit_proto::moby::buildkit::v1::frontend::{ - result::Result as RefResult, ReadFileRequest, ResolveImageConfigRequest, Result as Output, - ReturnRequest, SolveRequest, + result::Result as RefResult, ReadFileRequest, Ref, ResolveImageConfigRequest, + Result as Output, ReturnRequest, SolveRequest, }; pub use buildkit_llb::ops::source::{ImageSource, ResolveMode}; @@ -111,7 +111,7 @@ impl Bridge { }; match inner { - RefResult::Ref(inner) => Ok(OutputRef(inner)), + RefResult::Ref(Ref { id, .. }) => Ok(OutputRef(id)), other => bail!("Unexpected solve response: {:?}", other), } } @@ -164,7 +164,10 @@ impl Bridge { let request = ReturnRequest { error: None, result: Some(Output { - result: Some(RefResult::Ref(output.0)), + result: Some(RefResult::Ref(Ref { + id: output.0, + def: None, + })), metadata, ..Default::default() From 49666c39c5367923cbe566e41b22a54820fa52c0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 26 Apr 2026 16:35:21 +0000 Subject: [PATCH 05/21] buildkit-proto: restore gRPC client generation via tonic-build 0.12 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous v0.18 upgrade swapped tonic_build::compile for prost_build::compile_protos, which dropped generation of the LlbBridgeClient gRPC client expected by buildkit-frontend. Restore tonic-build, bump it to 0.12 (the first line compatible with prost 0.13.x), add tonic 0.12 as a runtime dep so the generated client code compiles, and switch the crate to edition 2021 (TryInto in prelude is required by tonic 0.12 generated code). Also set workspace.resolver = "2" so the proto crate's edition-2021 features apply consistently across the workspace. `cargo build -p buildkit-proto` succeeds and the generated module exposes `moby::buildkit::v1::frontend::llb_bridge_client::LlbBridgeClient` again. Note: buildkit-frontend still pins tonic 0.1 + tokio 0.2 + tower 0.3 + mio 0.6, which are incompatible with tonic 0.12. Bringing buildkit-frontend forward (tonic / tokio / hyper / tower upgrade and the stdio.rs mio→AsyncFd rewrite) is a follow-up beyond the proto update scope. https://claude.ai/code/session_01XtZHcL6rKJDuX7tUS3okdc --- Cargo.toml | 1 + buildkit-proto/Cargo.toml | 10 +++------- buildkit-proto/build.rs | 33 ++++++++++++++++++--------------- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index dc0a9a3..3dd0ffa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,5 @@ [workspace] +resolver = "2" members = [ "buildkit-proto", "buildkit-llb", diff --git a/buildkit-proto/Cargo.toml b/buildkit-proto/Cargo.toml index 42bd3a5..d6b6b62 100644 --- a/buildkit-proto/Cargo.toml +++ b/buildkit-proto/Cargo.toml @@ -2,7 +2,7 @@ name = "buildkit-proto" version = "0.2.0" authors = ["Denys Zariaiev "] -edition = "2018" +edition = "2021" description = "Protobuf interfaces to BuildKit" documentation = "https://docs.rs/buildkit-proto" @@ -15,11 +15,7 @@ license = "MIT/Apache-2.0" [dependencies] prost = "0.13.4" prost-types = "0.13.4" +tonic = "0.12" [build-dependencies] -prost-build = "0.13.4" - -[build-dependencies.tonic-build] -version = "0.1" -default-features = false -features = ["transport"] +tonic-build = "0.12" diff --git a/buildkit-proto/build.rs b/buildkit-proto/build.rs index e56e3a7..09eb17b 100644 --- a/buildkit-proto/build.rs +++ b/buildkit-proto/build.rs @@ -1,17 +1,20 @@ -use std::io::Result; -fn main() -> Result<()> { - prost_build::compile_protos( - &[ - "proto/github.com/moby/buildkit/api/types/worker.proto", - "proto/github.com/moby/buildkit/frontend/gateway/pb/gateway.proto", - "proto/github.com/moby/buildkit/solver/pb/ops.proto", - "proto/github.com/moby/buildkit/util/apicaps/pb/caps.proto", - "proto/github.com/moby/buildkit/sourcepolicy/pb/policy.proto", - "proto/google/rpc/status.proto", - "proto/github.com/gogo/protobuf/gogoproto/gogo.proto", - "proto/github.com/tonistiigi/fsutil/types/stat.proto", - ], - &["proto/"], - )?; +fn main() -> Result<(), Box> { + tonic_build::configure() + .build_client(true) + .build_server(false) + .compile_protos( + &[ + "proto/github.com/moby/buildkit/api/types/worker.proto", + "proto/github.com/moby/buildkit/frontend/gateway/pb/gateway.proto", + "proto/github.com/moby/buildkit/solver/pb/ops.proto", + "proto/github.com/moby/buildkit/util/apicaps/pb/caps.proto", + "proto/github.com/moby/buildkit/sourcepolicy/pb/policy.proto", + "proto/google/rpc/status.proto", + "proto/github.com/gogo/protobuf/gogoproto/gogo.proto", + "proto/github.com/tonistiigi/fsutil/types/stat.proto", + ], + &["proto/"], + )?; + Ok(()) } From 4f3b26b4e15f9a8ef562d2e603d45611eca14020 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Apr 2026 12:43:22 +0000 Subject: [PATCH 06/21] fix mismatched_lifetime_syntaxes warnings on modern rustc The newer `mismatched_lifetime_syntaxes` lint (deny-by-default through `#![deny(warnings)]`) flagged three sites where an elided lifetime on an input was paired with a hidden lifetime on a return type. Tighten each signature to use `'_` consistently so the relationship is explicit. https://claude.ai/code/session_01XtZHcL6rKJDuX7tUS3okdc --- buildkit-frontend/src/options/deserializer.rs | 2 +- buildkit-llb/examples/highly-parallel.rs | 2 +- buildkit-llb/src/ops/fs/copy.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/buildkit-frontend/src/options/deserializer.rs b/buildkit-frontend/src/options/deserializer.rs index 59b5673..ad3a0df 100644 --- a/buildkit-frontend/src/options/deserializer.rs +++ b/buildkit-frontend/src/options/deserializer.rs @@ -44,7 +44,7 @@ enum EnvValue<'de> { #[derive(Debug)] struct EnvItem<'de>(&'de str); -fn extract_name_and_value(mut raw_value: &str) -> (&str, EnvValue) { +fn extract_name_and_value(mut raw_value: &str) -> (&str, EnvValue<'_>) { if raw_value.starts_with("build-arg:") { raw_value = raw_value.trim_start_matches("build-arg:"); } diff --git a/buildkit-llb/examples/highly-parallel.rs b/buildkit-llb/examples/highly-parallel.rs index 82a2f2f..ed3e30f 100644 --- a/buildkit-llb/examples/highly-parallel.rs +++ b/buildkit-llb/examples/highly-parallel.rs @@ -40,7 +40,7 @@ fn main() { .unwrap() } -fn build_init_commands(image: &ImageSource) -> Vec { +fn build_init_commands(image: &ImageSource) -> Vec> { (0..100) .map(|idx| { let base_dir = format!("/file/{}", idx); diff --git a/buildkit-llb/src/ops/fs/copy.rs b/buildkit-llb/src/ops/fs/copy.rs index b677b9d..a4b2cae 100644 --- a/buildkit-llb/src/ops/fs/copy.rs +++ b/buildkit-llb/src/ops/fs/copy.rs @@ -48,7 +48,7 @@ impl OpWithoutSource { } } - pub fn from

(self, source: LayerPath<'_, P>) -> OpWithSource + pub fn from

(self, source: LayerPath<'_, P>) -> OpWithSource<'_> where P: AsRef, { From c6fc32b63ad6e91ed838b3c3835055ef4aa98ebc Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Apr 2026 12:43:35 +0000 Subject: [PATCH 07/21] buildkit-frontend: modernize transport stack for tonic 0.12 After cat 1 regenerated the buildkit-proto gRPC client with tonic 0.12, buildkit-frontend stayed pinned on tonic 0.1 / tokio 0.2 / tower 0.3, which caused the `Channel: Service>>` trait bound to fail at every `LlbBridgeClient` call site: the v0.1 Channel and the v0.12 generated client were incompatible. Bumps the runtime stack to match buildkit-proto: - tonic 0.1 -> 0.12 - tokio 0.2 -> 1 - tower 0.3 -> 0.5 (only the `util` feature is needed) - drop mio 0.6, pin-project 0.4, libc, bytes (no longer used) - add hyper-util 0.1 with the `tokio` feature so we can adapt our AsyncRead/Write socket to the hyper::rt::Read/Write that Endpoint::connect_with_connector now requires stdio.rs is rewritten on top of `tokio::io::{stdin, stdout}` (which became async in tokio 1.x), wrapped in `hyper_util::rt::TokioIo` at the connector boundary; the custom mio-based `Evented` plumbing is gone. The tokio runtime macros in the examples switch to the `flavor = "..."` syntax. An unused `ResolveMode` re-export is removed. `cargo build --workspace --all-targets` and `cargo test --workspace` both pass (29 tests). https://claude.ai/code/session_01XtZHcL6rKJDuX7tUS3okdc --- buildkit-frontend/Cargo.toml | 17 ++- buildkit-frontend/examples/download.rs | 2 +- buildkit-frontend/examples/ssh-mount.rs | 2 +- buildkit-frontend/src/bridge.rs | 2 +- buildkit-frontend/src/stdio.rs | 159 ++++-------------------- 5 files changed, 37 insertions(+), 145 deletions(-) diff --git a/buildkit-frontend/Cargo.toml b/buildkit-frontend/Cargo.toml index eb340e3..efaf68d 100644 --- a/buildkit-frontend/Cargo.toml +++ b/buildkit-frontend/Cargo.toml @@ -13,22 +13,19 @@ categories = ["development-tools::build-utils", "api-bindings"] license = "MIT/Apache-2.0" [dependencies] -bytes = "0.5" either = "1.5" failure = "0.1" futures = "0.3" -libc = "0.2" log = "0.4" -mio = "0.6" -pin-project = "0.4" serde_json = "1.0" -tonic = "0.1" -tower = "0.3" +tonic = "0.12" +tower = { version = "0.5", features = ["util"] } +hyper-util = { version = "0.1", features = ["tokio"] } [dependencies.tokio] -version = "0.2" +version = "1" default-features = false -features = ["io-std"] +features = ["sync", "io-std", "io-util"] [dependencies.serde] version = "1.0" @@ -54,5 +51,5 @@ regex = "1.3" url = "2.1" [dev-dependencies.tokio] -version = "0.2" -features = ["macros", "rt-core", "rt-threaded"] +version = "1" +features = ["macros", "rt-multi-thread"] diff --git a/buildkit-frontend/examples/download.rs b/buildkit-frontend/examples/download.rs index 4047c44..b29d62e 100644 --- a/buildkit-frontend/examples/download.rs +++ b/buildkit-frontend/examples/download.rs @@ -13,7 +13,7 @@ use buildkit_frontend::{Bridge, Frontend, FrontendOutput, OutputRef}; use buildkit_llb::prelude::*; -#[tokio::main(threaded_scheduler)] +#[tokio::main(flavor = "multi_thread")] async fn main() { env_logger::init(); diff --git a/buildkit-frontend/examples/ssh-mount.rs b/buildkit-frontend/examples/ssh-mount.rs index e75f738..797df2f 100644 --- a/buildkit-frontend/examples/ssh-mount.rs +++ b/buildkit-frontend/examples/ssh-mount.rs @@ -7,7 +7,7 @@ use buildkit_frontend::{Bridge, Frontend, FrontendOutput, Options, OutputRef}; use buildkit_llb::prelude::*; -#[tokio::main(basic_scheduler)] +#[tokio::main(flavor = "current_thread")] async fn main() { env_logger::init(); diff --git a/buildkit-frontend/src/bridge.rs b/buildkit-frontend/src/bridge.rs index 2782eed..85fbebe 100644 --- a/buildkit-frontend/src/bridge.rs +++ b/buildkit-frontend/src/bridge.rs @@ -16,7 +16,7 @@ use buildkit_proto::moby::buildkit::v1::frontend::{ Result as Output, ReturnRequest, SolveRequest, }; -pub use buildkit_llb::ops::source::{ImageSource, ResolveMode}; +pub use buildkit_llb::ops::source::ImageSource; pub use buildkit_llb::ops::Terminal; pub use buildkit_proto::moby::buildkit::v1::frontend::FileRange; diff --git a/buildkit-frontend/src/stdio.rs b/buildkit-frontend/src/stdio.rs index e9fd194..5746db2 100644 --- a/buildkit-frontend/src/stdio.rs +++ b/buildkit-frontend/src/stdio.rs @@ -1,165 +1,60 @@ -use std::io::{self, stdin, stdout}; +use std::io; use std::pin::Pin; use std::task::{Context, Poll}; -use pin_project::pin_project; -use tokio::io::*; +use hyper_util::rt::TokioIo; +use tokio::io::{stdin, stdout, AsyncRead, AsyncWrite, ReadBuf, Stdin, Stdout}; +use tonic::transport::server::Connected; use tonic::transport::Uri; -#[pin_project] pub struct StdioSocket { - #[pin] - reader: PollEvented, - - #[pin] - writer: PollEvented, + reader: Stdin, + writer: Stdout, } -pub async fn stdio_connector(_: Uri) -> io::Result { - StdioSocket::try_new() +pub async fn stdio_connector(_: Uri) -> io::Result> { + StdioSocket::try_new().map(TokioIo::new) } impl StdioSocket { pub fn try_new() -> io::Result { Ok(StdioSocket { - reader: PollEvented::new(async_stdio::EventedStdin::try_new(stdin())?)?, - writer: PollEvented::new(async_stdio::EventedStdout::try_new(stdout())?)?, + reader: stdin(), + writer: stdout(), }) } } impl AsyncRead for StdioSocket { fn poll_read( - self: Pin<&mut Self>, + mut self: Pin<&mut Self>, cx: &mut Context<'_>, - buf: &mut [u8], - ) -> Poll> { - self.project().reader.poll_read(cx, buf) + buf: &mut ReadBuf<'_>, + ) -> Poll> { + Pin::new(&mut self.reader).poll_read(cx, buf) } } impl AsyncWrite for StdioSocket { - fn poll_write(self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll> { - self.project().writer.poll_write(cx, buf) + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + Pin::new(&mut self.writer).poll_write(cx, buf) } - fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - self.project().writer.poll_flush(cx) + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut self.writer).poll_flush(cx) } - fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - self.project().writer.poll_shutdown(cx) + fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Pin::new(&mut self.writer).poll_shutdown(cx) } } -mod async_stdio { - use std::io::{self, Read, Stdin, Stdout, Write}; - use std::os::unix::io::AsRawFd; - - use mio::event::Evented; - use mio::unix::EventedFd; - use mio::{Poll, PollOpt, Ready, Token}; - - use libc::{fcntl, F_GETFL, F_SETFL, O_NONBLOCK}; - - pub struct EventedStdin(Stdin); - pub struct EventedStdout(Stdout); - - impl EventedStdin { - pub fn try_new(stdin: Stdin) -> io::Result { - set_non_blocking_flag(&stdin)?; - - Ok(EventedStdin(stdin)) - } - } - - impl EventedStdout { - pub fn try_new(stdout: Stdout) -> io::Result { - set_non_blocking_flag(&stdout)?; - - Ok(EventedStdout(stdout)) - } - } - - impl Evented for EventedStdin { - fn register( - &self, - poll: &Poll, - token: Token, - interest: Ready, - opts: PollOpt, - ) -> io::Result<()> { - EventedFd(&self.0.as_raw_fd()).register(poll, token, interest, opts) - } - - fn reregister( - &self, - poll: &Poll, - token: Token, - interest: Ready, - opts: PollOpt, - ) -> io::Result<()> { - EventedFd(&self.0.as_raw_fd()).reregister(poll, token, interest, opts) - } - - fn deregister(&self, poll: &Poll) -> io::Result<()> { - EventedFd(&self.0.as_raw_fd()).deregister(poll) - } - } - - impl Read for EventedStdin { - fn read(&mut self, buf: &mut [u8]) -> io::Result { - self.0.read(buf) - } - } - - impl Evented for EventedStdout { - fn register( - &self, - poll: &Poll, - token: Token, - interest: Ready, - opts: PollOpt, - ) -> io::Result<()> { - EventedFd(&self.0.as_raw_fd()).register(poll, token, interest, opts) - } - - fn reregister( - &self, - poll: &Poll, - token: Token, - interest: Ready, - opts: PollOpt, - ) -> io::Result<()> { - EventedFd(&self.0.as_raw_fd()).reregister(poll, token, interest, opts) - } - - fn deregister(&self, poll: &Poll) -> io::Result<()> { - EventedFd(&self.0.as_raw_fd()).deregister(poll) - } - } +impl Connected for StdioSocket { + type ConnectInfo = (); - impl Write for EventedStdout { - fn write(&mut self, buf: &[u8]) -> io::Result { - self.0.write(buf) - } - - fn flush(&mut self) -> io::Result<()> { - self.0.flush() - } - } - - fn set_non_blocking_flag(stream: &T) -> io::Result<()> { - let flags = unsafe { fcntl(stream.as_raw_fd(), F_GETFL, 0) }; - - if flags < 0 { - return Err(std::io::Error::last_os_error()); - } - - if unsafe { fcntl(stream.as_raw_fd(), F_SETFL, flags | O_NONBLOCK) } != 0 { - return Err(std::io::Error::last_os_error()); - } - - Ok(()) - } + fn connect_info(&self) -> Self::ConnectInfo {} } From 81c523772f121e69861a75beda28344db21687c1 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Apr 2026 12:47:32 +0000 Subject: [PATCH 08/21] fix clippy lints introduced by rustc upgrades The crates declare `#![deny(clippy::all)]`, and several lints have been added to clippy since the project was last touched, so `cargo clippy` failed with 35+ errors. Most were auto-fixable via `cargo clippy --fix` (idiomatic `From` instead of `Into`, `#[derive(Default)]` instead of a manual impl, redundant references / patterns, `strip_prefix` over manual slicing, ...). The rest are mechanical: - Drop unused lifetime parameters on impl blocks (`impl<'a> ...` where `'a` never appears in the impl signature) in `buildkit-llb/src/ops/{exec/command,fs/sequence,source/{git,http, image,local}}.rs`. - Switch `crate::...` to `$crate::...` inside the exported `check_op!` / `check_op_property!` macros so they keep referring to `buildkit_llb` when used from doctests / external test crates. - Allow `clippy::result_unit_err` on the `FileOperation` trait to preserve the public `Result<_, ()>` API. - Allow `clippy::upper_case_acronyms` on the `LLB` test enum variant. `cargo clippy --workspace --all-targets -- -D warnings` and `cargo test --workspace` both pass clean. https://claude.ai/code/session_01XtZHcL6rKJDuX7tUS3okdc --- buildkit-frontend/examples/download.rs | 8 +-- buildkit-frontend/examples/reverse.rs | 3 +- buildkit-frontend/examples/ssh-mount.rs | 18 +++--- buildkit-frontend/src/oci.rs | 34 +++++------ buildkit-frontend/src/options/common.rs | 14 ++--- buildkit-frontend/src/options/default.rs | 24 ++++---- buildkit-frontend/src/options/deserializer.rs | 4 +- buildkit-frontend/src/options/mod.rs | 1 + buildkit-llb/examples/highly-parallel.rs | 4 +- buildkit-llb/examples/network.rs | 2 +- buildkit-llb/examples/scratch-owned.rs | 2 +- buildkit-llb/examples/scratch.rs | 2 +- buildkit-llb/src/ops/exec/command.rs | 2 +- buildkit-llb/src/ops/exec/context.rs | 14 ++--- buildkit-llb/src/ops/exec/mod.rs | 14 ++--- buildkit-llb/src/ops/fs/mod.rs | 1 + buildkit-llb/src/ops/fs/sequence.rs | 8 +-- buildkit-llb/src/ops/source/git.rs | 2 +- buildkit-llb/src/ops/source/http.rs | 2 +- buildkit-llb/src/ops/source/image.rs | 10 +--- buildkit-llb/src/ops/source/local.rs | 2 +- buildkit-llb/src/ops/terminal.rs | 10 ++-- buildkit-llb/src/serialization/output.rs | 2 +- buildkit-llb/src/utils.rs | 60 +++++++++---------- 24 files changed, 118 insertions(+), 125 deletions(-) diff --git a/buildkit-frontend/examples/download.rs b/buildkit-frontend/examples/download.rs index b29d62e..c147068 100644 --- a/buildkit-frontend/examples/download.rs +++ b/buildkit-frontend/examples/download.rs @@ -17,7 +17,7 @@ use buildkit_llb::prelude::*; async fn main() { env_logger::init(); - if let Err(_) = run_frontend(DownloadFrontend).await { + if run_frontend(DownloadFrontend).await.is_err() { std::process::exit(1); } } @@ -102,7 +102,7 @@ impl DownloadFrontend { let alpine = Source::image("alpine:latest").ref_counted(); let builder_rootfs = Command::run("apk") - .args(&["add", "curl"]) + .args(["add", "curl"]) .custom_name("Installing curl") .mount(Mount::Layer(OutputIdx(0), alpine.output(), "/")) .ref_counted(); @@ -113,7 +113,7 @@ impl DownloadFrontend { let full_path = PathBuf::from(OUTPUT_DIR).join(&relative_path); let op = Command::run("curl") - .args(&[&url.to_string(), "-o", &full_path.to_string_lossy()]) + .args([url.as_ref(), "-o", &full_path.to_string_lossy()]) .mount(Mount::ReadOnlyLayer(builder_rootfs.output(0), "/")) .mount(Mount::Scratch(OutputIdx(0), OUTPUT_DIR)) .custom_name(format!("Downloading '{}'", relative_path.display())) @@ -149,7 +149,7 @@ impl DownloadFrontend { let cmd_regex = Regex::new(r#"Download\s+"(.+)"\s+as\s+"(.+)""#).unwrap(); dockerfile.lines().filter_map(move |line| { - let captures = cmd_regex.captures(&line)?; + let captures = cmd_regex.captures(line)?; Some(Url::parse(&captures[1]).map(|url| (url, captures[2].into()))) }) } diff --git a/buildkit-frontend/examples/reverse.rs b/buildkit-frontend/examples/reverse.rs index 5e4e5dc..e8393bc 100644 --- a/buildkit-frontend/examples/reverse.rs +++ b/buildkit-frontend/examples/reverse.rs @@ -13,7 +13,7 @@ use buildkit_llb::prelude::*; async fn main() { env_logger::init(); - if let Err(_) = run_frontend(ReverseFrontend).await { + if run_frontend(ReverseFrontend).await.is_err() { std::process::exit(1); } } @@ -72,7 +72,6 @@ impl ReverseFrontend { let transformed_contents: String = { String::from_utf8_lossy(&dockerfile_contents) .lines() - .into_iter() .map(|line| { line.trim() .chars() diff --git a/buildkit-frontend/examples/ssh-mount.rs b/buildkit-frontend/examples/ssh-mount.rs index 797df2f..0df8e07 100644 --- a/buildkit-frontend/examples/ssh-mount.rs +++ b/buildkit-frontend/examples/ssh-mount.rs @@ -11,7 +11,7 @@ use buildkit_llb::prelude::*; async fn main() { env_logger::init(); - if let Err(_) = run_frontend(ReverseFrontend).await { + if run_frontend(ReverseFrontend).await.is_err() { std::process::exit(1); } } @@ -76,23 +76,23 @@ impl ReverseFrontend { let mut test = None; for line in dockerfile_contents.lines() { - if line.starts_with("REPO:") { - repo = Some(line[5..].trim()); + if let Some(stripped) = line.strip_prefix("REPO:") { + repo = Some(stripped.trim()); } - if line.starts_with("TAG:") { - tag = Some(line[4..].trim()); + if let Some(stripped) = line.strip_prefix("TAG:") { + tag = Some(stripped.trim()); } - if line.starts_with("TEST:") { - test = Some(line[5..].trim()); + if let Some(stripped) = line.strip_prefix("TEST:") { + test = Some(stripped.trim()); } } let rootfs = Source::image("rust:latest"); let install_command = match (repo, tag) { (Some(repo), Some(tag)) => Command::run("cargo") - .args(&["install", "--git", repo, "--tag", tag]) + .args(["install", "--git", repo, "--tag", tag]) .mount(Mount::Layer(OutputIdx(0), rootfs.output(), "/")) .mount(Mount::OptionalSshAgent("/tmp/ssh_agent.0")) .env("PATH", PATH) @@ -108,7 +108,7 @@ impl ReverseFrontend { let test_command = if let Some(test) = test { Command::run("/bin/sh") - .args(&["-c", &format!("{} > {}", test, OUTPUT_FILENAME)]) + .args(["-c", &format!("{} > {}", test, OUTPUT_FILENAME)]) .mount(Mount::Layer(OutputIdx(0), install_command.output(0), "/")) .env("PATH", PATH) } else { diff --git a/buildkit-frontend/src/oci.rs b/buildkit-frontend/src/oci.rs index f398811..db78ba7 100644 --- a/buildkit-frontend/src/oci.rs +++ b/buildkit-frontend/src/oci.rs @@ -210,9 +210,9 @@ impl TryFrom for ExposedPort { } } -impl Into for ExposedPort { - fn into(self) -> String { - match self { +impl From for String { + fn from(val: ExposedPort) -> Self { + match val { ExposedPort::Tcp(port) => format!("{}/tcp", port), ExposedPort::Udp(port) => format!("{}/udp", port), } @@ -291,40 +291,40 @@ impl From for ImageConfig { exposed_ports: raw .exposed_ports - .map(|inner| inner.into_iter().map(|(port, _)| port).collect()), + .map(|inner| inner.into_keys().collect()), volumes: raw .volumes - .map(|inner| inner.into_iter().map(|(volume, _)| volume).collect()), + .map(|inner| inner.into_keys().collect()), } } } -impl Into for ImageConfig { - fn into(self) -> RawImageConfig { +impl From for RawImageConfig { + fn from(val: ImageConfig) -> Self { RawImageConfig { - user: self.user, - entrypoint: self.entrypoint, - cmd: self.cmd, - working_dir: self.working_dir, - labels: self.labels, - stop_signal: self.stop_signal, - - env: self.env.map(|inner| { + user: val.user, + entrypoint: val.entrypoint, + cmd: val.cmd, + working_dir: val.working_dir, + labels: val.labels, + stop_signal: val.stop_signal, + + env: val.env.map(|inner| { inner .into_iter() .map(|(key, value)| format!("{}={}", key, value)) .collect() }), - exposed_ports: self.exposed_ports.map(|inner| { + exposed_ports: val.exposed_ports.map(|inner| { inner .into_iter() .map(|port| (port, Value::Object(Default::default()))) .collect() }), - volumes: self.volumes.map(|inner| { + volumes: val.volumes.map(|inner| { inner .into_iter() .map(|volume| (volume, Value::Object(Default::default()))) diff --git a/buildkit-frontend/src/options/common.rs b/buildkit-frontend/src/options/common.rs index cc86031..a529141 100644 --- a/buildkit-frontend/src/options/common.rs +++ b/buildkit-frontend/src/options/common.rs @@ -54,18 +54,18 @@ impl CacheOptionsEntry { } } -impl Into for CacheOptionsEntry { - fn into(self) -> CacheOptionsEntryProto { +impl From for CacheOptionsEntryProto { + fn from(val: CacheOptionsEntry) -> Self { CacheOptionsEntryProto { - r#type: self.cache_type.into(), - attrs: self.attrs, + r#type: val.cache_type.into(), + attrs: val.attrs, } } } -impl Into for CacheType { - fn into(self) -> String { - match self { +impl From for String { + fn from(val: CacheType) -> Self { + match val { CacheType::Local => "local".into(), CacheType::Registry => "registry".into(), CacheType::Inline => "inline".into(), diff --git a/buildkit-frontend/src/options/default.rs b/buildkit-frontend/src/options/default.rs index 9923a32..4ef93f2 100644 --- a/buildkit-frontend/src/options/default.rs +++ b/buildkit-frontend/src/options/default.rs @@ -157,10 +157,10 @@ mod tests { ])) .unwrap(); - assert_eq!(options.has("option1"), true); - assert_eq!(options.has("option2"), true); - assert_eq!(options.has("option3"), false); - assert_eq!(options.has("option4"), true); + assert!(options.has("option1")); + assert!(options.has("option2")); + assert!(!options.has("option3")); + assert!(options.has("option4")); } #[test] @@ -172,14 +172,14 @@ mod tests { ])) .unwrap(); - assert_eq!(options.has_value("option1", ""), false); - assert_eq!(options.has_value("option1", "any_other"), false); - assert_eq!(options.has_value("option2", ""), false); - assert_eq!(options.has_value("option2", "any_other"), false); - assert_eq!(options.has_value("option3", "true"), true); - assert_eq!(options.has_value("option3", "false"), true); - assert_eq!(options.has_value("option3", "any_other"), true); - assert_eq!(options.has_value("option3", "missing"), false); + assert!(!options.has_value("option1", "")); + assert!(!options.has_value("option1", "any_other")); + assert!(!options.has_value("option2", "")); + assert!(!options.has_value("option2", "any_other")); + assert!(options.has_value("option3", "true")); + assert!(options.has_value("option3", "false")); + assert!(options.has_value("option3", "any_other")); + assert!(!options.has_value("option3", "missing")); } #[test] diff --git a/buildkit-frontend/src/options/deserializer.rs b/buildkit-frontend/src/options/deserializer.rs index ad3a0df..5c4ac7c 100644 --- a/buildkit-frontend/src/options/deserializer.rs +++ b/buildkit-frontend/src/options/deserializer.rs @@ -23,7 +23,7 @@ where }; let deserializer = EnvDeserializer { - vals: pairs.map(|value| extract_name_and_value(&value)), + vals: pairs.map(|value| extract_name_and_value(value)), }; T::deserialize(deserializer).map_err(Error::from) @@ -54,7 +54,7 @@ fn extract_name_and_value(mut raw_value: &str) -> (&str, EnvValue<'_>) { match parts.next() { None => (name, EnvValue::Flag), - Some(text) if text.is_empty() => (name, EnvValue::Flag), + Some("") => (name, EnvValue::Flag), Some(text) if &text[0..1] == "[" || &text[0..1] == "{" => (name, EnvValue::Json(text)), Some(text) => (name, EnvValue::Text(text)), } diff --git a/buildkit-frontend/src/options/mod.rs b/buildkit-frontend/src/options/mod.rs index 791c0e8..a6a8092 100644 --- a/buildkit-frontend/src/options/mod.rs +++ b/buildkit-frontend/src/options/mod.rs @@ -16,6 +16,7 @@ mod tests { #[derive(Debug, Deserialize, PartialEq)] #[serde(untagged)] #[serde(field_identifier, rename_all = "lowercase")] + #[allow(clippy::upper_case_acronyms)] enum Debug { All, LLB, diff --git a/buildkit-llb/examples/highly-parallel.rs b/buildkit-llb/examples/highly-parallel.rs index ed3e30f..7c5284e 100644 --- a/buildkit-llb/examples/highly-parallel.rs +++ b/buildkit-llb/examples/highly-parallel.rs @@ -53,7 +53,7 @@ fn build_init_commands(image: &ImageSource) -> Vec> { .ref_counted(); Command::run("/bin/sh") - .args(&["-c", &shell]) + .args(["-c", &shell]) .mount(Mount::ReadOnlyLayer(image.output(), "/")) .mount(Mount::Layer(OutputIdx(0), output_mount.output(0), "/out")) .ignore_cache(true) @@ -77,7 +77,7 @@ fn build_modify_commands<'a>( ); Command::run("/bin/sh") - .args(&["-c", &shell]) + .args(["-c", &shell]) .mount(Mount::ReadOnlyLayer(image.output(), "/")) .mount(Mount::Scratch(OutputIdx(0), "/out")) .mount(Mount::ReadOnlySelector( diff --git a/buildkit-llb/examples/network.rs b/buildkit-llb/examples/network.rs index 7e919da..87e38d4 100644 --- a/buildkit-llb/examples/network.rs +++ b/buildkit-llb/examples/network.rs @@ -9,7 +9,7 @@ fn main() { let alpine = Source::image("library/alpine:latest"); let bitflags_unpacked = { Command::run("/bin/tar") - .args(&[ + .args([ "-xvzC", "/out", "--strip-components=1", diff --git a/buildkit-llb/examples/scratch-owned.rs b/buildkit-llb/examples/scratch-owned.rs index 349c021..fc378a6 100644 --- a/buildkit-llb/examples/scratch-owned.rs +++ b/buildkit-llb/examples/scratch-owned.rs @@ -15,7 +15,7 @@ fn build_graph() -> OperationOutput<'static> { let command = { Command::run("/bin/sh") - .args(&["-c", "echo 'test string 5' > /out/file0"]) + .args(["-c", "echo 'test string 5' > /out/file0"]) .custom_name("create a dummy file") .mount(Mount::ReadOnlyLayer(builder_image.output(), "/")) .mount(Mount::Scratch(OutputIdx(0), "/out")) diff --git a/buildkit-llb/examples/scratch.rs b/buildkit-llb/examples/scratch.rs index 51a5c3e..f632e76 100644 --- a/buildkit-llb/examples/scratch.rs +++ b/buildkit-llb/examples/scratch.rs @@ -8,7 +8,7 @@ fn main() { let command = { Command::run("/bin/sh") - .args(&["-c", "echo 'test string 5' > /out/file0"]) + .args(["-c", "echo 'test string 5' > /out/file0"]) .custom_name("create a dummy file") .mount(Mount::ReadOnlyLayer(builder_image.output(), "/")) .mount(Mount::Scratch(OutputIdx(0), "/out")) diff --git a/buildkit-llb/src/ops/exec/command.rs b/buildkit-llb/src/ops/exec/command.rs index c262b59..95b35db 100644 --- a/buildkit-llb/src/ops/exec/command.rs +++ b/buildkit-llb/src/ops/exec/command.rs @@ -131,7 +131,7 @@ impl<'a> Command<'a> { } } -impl<'a, 'b: 'a> MultiBorrowedOutput<'b> for Command<'b> { +impl<'b> MultiBorrowedOutput<'b> for Command<'b> { fn output(&'b self, index: u32) -> OperationOutput<'b> { // TODO: check if the requested index available. OperationOutput::borrowed(self, OutputIdx(index)) diff --git a/buildkit-llb/src/ops/exec/context.rs b/buildkit-llb/src/ops/exec/context.rs index b000008..6384902 100644 --- a/buildkit-llb/src/ops/exec/context.rs +++ b/buildkit-llb/src/ops/exec/context.rs @@ -30,18 +30,18 @@ impl Context { } } -impl Into for Context { - fn into(self) -> Meta { +impl From for Meta { + fn from(val: Context) -> Self { Meta { args: { - once(self.name.clone()) - .chain(self.args.iter().cloned()) + once(val.name.clone()) + .chain(val.args.iter().cloned()) .collect() }, - env: self.env, - cwd: self.cwd.to_string_lossy().into(), - user: self.user, + env: val.env, + cwd: val.cwd.to_string_lossy().into(), + user: val.user, ..Default::default() } diff --git a/buildkit-llb/src/ops/exec/mod.rs b/buildkit-llb/src/ops/exec/mod.rs index 4db1bd7..6fe2011 100644 --- a/buildkit-llb/src/ops/exec/mod.rs +++ b/buildkit-llb/src/ops/exec/mod.rs @@ -13,7 +13,7 @@ fn serialization() { crate::check_op!( { Command::run("/bin/sh") - .args(&["-c", "echo 'test string' > /out/file0"]) + .args(["-c", "echo 'test string' > /out/file0"]) .env("HOME", "/root") .custom_name("exec custom name") }, @@ -53,7 +53,7 @@ fn serialization_with_env_iter() { crate::check_op!( { - Command::run("cargo").args(&["build"]).env_iter(vec![ + Command::run("cargo").args(["build"]).env_iter(vec![ ("HOME", "/root"), ("PATH", "/bin"), ("CARGO_HOME", "/root/.cargo"), @@ -94,7 +94,7 @@ fn serialization_with_cwd() { use buildkit_proto::pb::{op::Op, ExecOp, Meta, NetMode, SecurityMode}; crate::check_op!( - Command::run("cargo").args(&["build"]).cwd("/rust-src"), + Command::run("cargo").args(["build"]).cwd("/rust-src"), |digest| { "sha256:b8120a0e1d1f7fcaa3d6c95db292d064524dc92c6cae8b97672d4e1eafcd03fa" }, |description| { vec![] }, |caps| { vec![] }, @@ -124,7 +124,7 @@ fn serialization_with_user() { use buildkit_proto::pb::{op::Op, ExecOp, Meta, NetMode, SecurityMode}; crate::check_op!( - Command::run("cargo").args(&["build"]).user("builder"), + Command::run("cargo").args(["build"]).user("builder"), |digest| { "sha256:7631ea645e2126e9dbc5d9ae789e34301d9d5c80ce89bfa72bc9b82aa43b57c0" }, |description| { vec![] }, |caps| { vec![] }, @@ -160,7 +160,7 @@ fn serialization_with_mounts() { let final_image = Source::image("library/alpine:latest"); let command = Command::run("cargo") - .args(&["build"]) + .args(["build"]) .mount(Mount::ReadOnlyLayer(builder_image.output(), "/")) .mount(Mount::Scratch(OutputIdx(1), "/tmp")) .mount(Mount::ReadOnlySelector( @@ -297,7 +297,7 @@ fn serialization_with_several_root_mounts() { let final_image = Source::image("library/alpine:latest"); let command = Command::run("cargo") - .args(&["build"]) + .args(["build"]) .mount(Mount::Scratch(OutputIdx(0), "/tmp")) .mount(Mount::ReadOnlyLayer(builder_image.output(), "/")) .mount(Mount::Scratch(OutputIdx(1), "/var")) @@ -379,7 +379,7 @@ fn serialization_with_ssh_mounts() { let builder_image = Source::image("rustlang/rust:nightly"); let command = Command::run("cargo") - .args(&["build"]) + .args(["build"]) .mount(Mount::ReadOnlyLayer(builder_image.output(), "/")) .mount(Mount::OptionalSshAgent("/run/buildkit/ssh_agent.0")); diff --git a/buildkit-llb/src/ops/fs/mod.rs b/buildkit-llb/src/ops/fs/mod.rs index ddf66db..df55385 100644 --- a/buildkit-llb/src/ops/fs/mod.rs +++ b/buildkit-llb/src/ops/fs/mod.rs @@ -46,6 +46,7 @@ impl FileSystem { } } +#[allow(clippy::result_unit_err)] pub trait FileOperation: Debug + Send + Sync { fn output(&self) -> i32; diff --git a/buildkit-llb/src/ops/fs/sequence.rs b/buildkit-llb/src/ops/fs/sequence.rs index fe38515..e6908b9 100644 --- a/buildkit-llb/src/ops/fs/sequence.rs +++ b/buildkit-llb/src/ops/fs/sequence.rs @@ -48,14 +48,12 @@ impl<'a> SequenceOperation<'a> { // TODO: make sure the `inner` elements have monotonic indexes self.inner - .iter() - .filter(|fs| fs.output() >= 0) - .last() + .iter().rfind(|fs| fs.output() >= 0) .map(|fs| fs.output() as u32) } } -impl<'a, 'b: 'a> MultiBorrowedOutput<'b> for SequenceOperation<'b> { +impl<'b> MultiBorrowedOutput<'b> for SequenceOperation<'b> { fn output(&'b self, index: u32) -> OperationOutput<'b> { // TODO: check if the requested index available. OperationOutput::borrowed(self, OutputIdx(index)) @@ -69,7 +67,7 @@ impl<'a> MultiOwnedOutput<'a> for Arc> { } } -impl<'a, 'b: 'a> MultiBorrowedLastOutput<'b> for SequenceOperation<'b> { +impl<'b> MultiBorrowedLastOutput<'b> for SequenceOperation<'b> { fn last_output(&'b self) -> Option> { self.last_output_index().map(|index| self.output(index)) } diff --git a/buildkit-llb/src/ops/source/git.rs b/buildkit-llb/src/ops/source/git.rs index 7a8f466..e3df6d2 100644 --- a/buildkit-llb/src/ops/source/git.rs +++ b/buildkit-llb/src/ops/source/git.rs @@ -60,7 +60,7 @@ impl<'a> SingleBorrowedOutput<'a> for GitSource { } } -impl<'a> SingleOwnedOutput<'static> for Arc { +impl SingleOwnedOutput<'static> for Arc { fn output(&self) -> OperationOutput<'static> { OperationOutput::owned(self.clone(), OutputIdx(0)) } diff --git a/buildkit-llb/src/ops/source/http.rs b/buildkit-llb/src/ops/source/http.rs index 81b8f07..98c4350 100644 --- a/buildkit-llb/src/ops/source/http.rs +++ b/buildkit-llb/src/ops/source/http.rs @@ -47,7 +47,7 @@ impl<'a> SingleBorrowedOutput<'a> for HttpSource { } } -impl<'a> SingleOwnedOutput<'static> for Arc { +impl SingleOwnedOutput<'static> for Arc { fn output(&self) -> OperationOutput<'static> { OperationOutput::owned(self.clone(), OutputIdx(0)) } diff --git a/buildkit-llb/src/ops/source/image.rs b/buildkit-llb/src/ops/source/image.rs index a125610..bfa7c22 100644 --- a/buildkit-llb/src/ops/source/image.rs +++ b/buildkit-llb/src/ops/source/image.rs @@ -24,8 +24,9 @@ pub struct ImageSource { resolve_mode: Option, } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Default)] pub enum ResolveMode { + #[default] Default, ForcePull, PreferLocal, @@ -41,11 +42,6 @@ impl fmt::Display for ResolveMode { } } -impl Default for ResolveMode { - fn default() -> Self { - ResolveMode::Default - } -} lazy_static! { static ref TAG_EXPR: Regex = Regex::new(r":[\w][\w.-]+$").unwrap(); @@ -171,7 +167,7 @@ impl<'a> SingleBorrowedOutput<'a> for ImageSource { } } -impl<'a> SingleOwnedOutput<'static> for Arc { +impl SingleOwnedOutput<'static> for Arc { fn output(&self) -> OperationOutput<'static> { OperationOutput::owned(self.clone(), OutputIdx(0)) } diff --git a/buildkit-llb/src/ops/source/local.rs b/buildkit-llb/src/ops/source/local.rs index 9b948c5..d92496b 100644 --- a/buildkit-llb/src/ops/source/local.rs +++ b/buildkit-llb/src/ops/source/local.rs @@ -57,7 +57,7 @@ impl<'a> SingleBorrowedOutput<'a> for LocalSource { } } -impl<'a> SingleOwnedOutput<'static> for Arc { +impl SingleOwnedOutput<'static> for Arc { fn output(&self) -> OperationOutput<'static> { OperationOutput::owned(self.clone(), OutputIdx(0)) } diff --git a/buildkit-llb/src/ops/terminal.rs b/buildkit-llb/src/ops/terminal.rs index d073ba8..7f236dc 100644 --- a/buildkit-llb/src/ops/terminal.rs +++ b/buildkit-llb/src/ops/terminal.rs @@ -66,13 +66,13 @@ fn serialization() { let final_image = Source::image("library/alpine:latest"); let first_command = Command::run("rustc") - .args(&["--crate-name", "crate-1"]) + .args(["--crate-name", "crate-1"]) .mount(Mount::ReadOnlyLayer(builder_image.output(), "/")) .mount(Mount::ReadOnlyLayer(context.output(), "/context")) .mount(Mount::Scratch(OutputIdx(0), "/target")); let second_command = Command::run("rustc") - .args(&["--crate-name", "crate-2"]) + .args(["--crate-name", "crate-2"]) .mount(Mount::ReadOnlyLayer(builder_image.output(), "/")) .mount(Mount::ReadOnlyLayer(context.output(), "/context")) .mount(Mount::Scratch(OutputIdx(0), "/target")); @@ -108,7 +108,7 @@ fn serialization() { definition .def .iter() - .map(|bytes| Node::get_digest(&bytes)) + .map(|bytes| Node::get_digest(bytes)) .collect::>(), crate::utils::test::to_vec(vec![ "sha256:a60212791641cbeaa3a49de4f7dff9e40ae50ec19d1be9607232037c1db16702", @@ -123,9 +123,7 @@ fn serialization() { let mut metadata_digests = { definition - .metadata - .iter() - .map(|(digest, _)| digest.as_str()) + .metadata.keys().map(|digest| digest.as_str()) .collect::>() }; diff --git a/buildkit-llb/src/serialization/output.rs b/buildkit-llb/src/serialization/output.rs index d67e9f7..a2d3343 100644 --- a/buildkit-llb/src/serialization/output.rs +++ b/buildkit-llb/src/serialization/output.rs @@ -23,7 +23,7 @@ impl Node { pub fn get_digest(bytes: &[u8]) -> String { let mut hasher = Sha256::new(); - hasher.update(&bytes); + hasher.update(bytes); format!("sha256:{:x}", hasher.finalize()) } diff --git a/buildkit-llb/src/utils.rs b/buildkit-llb/src/utils.rs index 28e3d5e..eefb607 100644 --- a/buildkit-llb/src/utils.rs +++ b/buildkit-llb/src/utils.rs @@ -35,7 +35,7 @@ impl<'a> OperationOutput<'a> { pub(crate) fn operation(&self) -> &dyn Operation { match self.kind { OperationOutputKind::Owned(ref op, ..) => op.as_ref(), - OperationOutputKind::Borrowed(ref op, ..) => *op, + OperationOutputKind::Borrowed(op, ..) => op, } } @@ -48,47 +48,47 @@ impl<'a> OperationOutput<'a> { } } -impl Into for OutputIdx { - fn into(self) -> i64 { - self.0.into() +impl From for i64 { + fn from(val: OutputIdx) -> Self { + val.0.into() } } -impl Into for &OutputIdx { - fn into(self) -> i64 { - self.0.into() +impl From<&OutputIdx> for i64 { + fn from(val: &OutputIdx) -> Self { + val.0.into() } } -impl Into for OwnOutputIdx { - fn into(self) -> i64 { - self.0.into() +impl From for i64 { + fn from(val: OwnOutputIdx) -> Self { + val.0.into() } } -impl Into for &OwnOutputIdx { - fn into(self) -> i64 { - self.0.into() +impl From<&OwnOutputIdx> for i64 { + fn from(val: &OwnOutputIdx) -> Self { + val.0.into() } } -impl Into for OutputIdx { - fn into(self) -> i32 { - self.0 as i32 +impl From for i32 { + fn from(val: OutputIdx) -> Self { + val.0 as i32 } } -impl Into for &OutputIdx { - fn into(self) -> i32 { - self.0 as i32 +impl From<&OutputIdx> for i32 { + fn from(val: &OutputIdx) -> Self { + val.0 as i32 } } -impl Into for OwnOutputIdx { - fn into(self) -> i32 { - self.0 as i32 +impl From for i32 { + fn from(val: OwnOutputIdx) -> Self { + val.0 as i32 } } -impl Into for &OwnOutputIdx { - fn into(self) -> i32 { - self.0 as i32 +impl From<&OwnOutputIdx> for i32 { + fn from(val: &OwnOutputIdx) -> Self { + val.0 as i32 } } @@ -99,12 +99,12 @@ pub mod test { ($op:expr, $(|$name:ident| $value:expr,)*) => ($crate::check_op!($op, $(|$name| $value),*)); ($op:expr, $(|$name:ident| $value:expr),*) => {{ #[allow(unused_imports)] - use crate::serialization::{Context, Operation}; + use $crate::serialization::{Context, Operation}; let mut context = Context::default(); let serialized = $op.serialize(&mut context).unwrap(); - $(crate::check_op_property!(serialized, context, $name, $value));* + $($crate::check_op_property!(serialized, context, $name, $value));* }}; } @@ -148,7 +148,7 @@ pub mod test { .registered_nodes_iter() .map(|node| node.digest.clone()) .collect::>(), - crate::utils::test::to_vec($value), + $crate::utils::test::to_vec($value), ); }; @@ -161,13 +161,13 @@ pub mod test { .collect::>(); caps.sort(); - assert_eq!(caps, crate::utils::test::to_vec($value)); + assert_eq!(caps, $crate::utils::test::to_vec($value)); }}; ($serialized:expr, $context:expr, description, $value:expr) => { assert_eq!( $serialized.metadata.description, - crate::utils::test::to_map($value), + $crate::utils::test::to_map($value), ); }; From 339c43755e0f4b603a18c672e9aeb75fc319c940 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Apr 2026 17:47:59 +0000 Subject: [PATCH 09/21] buildkit-llb: pin ops to a target platform via .platform() / .with_platform() Adds a new `crate::ops::platform` module that re-exports `buildkit_proto::pb::Platform` together with constructor helpers for the common targets (`linux_amd64`, `linux_arm64`, `linux_arm_v7`, `windows_amd64`, ...) and a `platform_id` helper that produces the canonical `/[/]` string used as a key in BuildKit's RefMap and in the `containerimage.config/` metadata key. - `Command::platform(Platform)` populates `pb::Op.platform` on exec ops so workers can be scheduled accordingly (cross-compilation). - `ImageSource::with_platform(Platform)` does the same on source ops and exposes `.platform()` so the frontend bridge can pass the constraint through to `ResolveImageConfigRequest.platform`. Adds a `platform` arm to the internal `check_op_property!` macro and three new serialization tests (one per builder + a unit test on `platform_id`). `cargo test -p buildkit-llb` runs 23 tests (was 20). https://claude.ai/code/session_01XtZHcL6rKJDuX7tUS3okdc --- buildkit-llb/src/lib.rs | 1 + buildkit-llb/src/ops/exec/command.rs | 12 +++ buildkit-llb/src/ops/exec/mod.rs | 40 ++++++++ buildkit-llb/src/ops/mod.rs | 2 + buildkit-llb/src/ops/platform.rs | 141 +++++++++++++++++++++++++++ buildkit-llb/src/ops/source/image.rs | 45 +++++++++ buildkit-llb/src/utils.rs | 14 +++ 7 files changed, 255 insertions(+) create mode 100644 buildkit-llb/src/ops/platform.rs diff --git a/buildkit-llb/src/lib.rs b/buildkit-llb/src/lib.rs index a34d416..4d551f9 100644 --- a/buildkit-llb/src/lib.rs +++ b/buildkit-llb/src/lib.rs @@ -17,6 +17,7 @@ pub mod utils; pub mod prelude { pub use crate::ops::exec::Mount; pub use crate::ops::fs::LayerPath; + pub use crate::ops::platform::{self, Platform}; pub use crate::ops::source::ResolveMode; pub use crate::ops::*; pub use crate::utils::{OperationOutput, OutputIdx, OwnOutputIdx}; diff --git a/buildkit-llb/src/ops/exec/command.rs b/buildkit-llb/src/ops/exec/command.rs index 95b35db..ea99616 100644 --- a/buildkit-llb/src/ops/exec/command.rs +++ b/buildkit-llb/src/ops/exec/command.rs @@ -11,6 +11,7 @@ use either::Either; use super::context::Context; use super::mount::Mount; +use crate::ops::platform::Platform; use crate::ops::{MultiBorrowedOutput, MultiOwnedOutput, OperationBuilder}; use crate::serialization::{Context as SerializationCtx, Node, Operation, OperationId, Result}; use crate::utils::{OperationOutput, OutputIdx}; @@ -27,6 +28,7 @@ pub struct Command<'a> { description: HashMap, caps: HashMap, ignore_cache: bool, + platform: Option, } impl<'a> Command<'a> { @@ -44,9 +46,18 @@ impl<'a> Command<'a> { description: Default::default(), caps: Default::default(), ignore_cache: false, + platform: None, } } + /// Pin this exec op to a specific platform. The op will only be scheduled + /// on a worker that advertises matching platform capabilities, which is + /// what enables cross-compilation in multi-platform builds. + pub fn platform(mut self, platform: Platform) -> Self { + self.platform = Some(platform); + self + } + pub fn args(mut self, args: A) -> Self where A: IntoIterator, @@ -304,6 +315,7 @@ impl<'a> Operation for Command<'a> { })), inputs: inputs.into_iter().flatten().collect(), + platform: self.platform.clone(), ..Default::default() }; diff --git a/buildkit-llb/src/ops/exec/mod.rs b/buildkit-llb/src/ops/exec/mod.rs index 6fe2011..a67ce67 100644 --- a/buildkit-llb/src/ops/exec/mod.rs +++ b/buildkit-llb/src/ops/exec/mod.rs @@ -372,6 +372,46 @@ fn serialization_with_several_root_mounts() { ); } +#[test] +fn serialization_with_platform() { + use crate::ops::platform; + use crate::prelude::*; + use buildkit_proto::pb::{op::Op, ExecOp, Meta, NetMode, Platform, SecurityMode}; + + crate::check_op!( + Command::run("/bin/sh") + .args(["-c", "echo arm"]) + .platform(platform::linux_arm64()), + |digest| { "sha256:2aa940f1054e900f52ccd50ff60d018c04855cc91d3470d469fb9cd3eaee10a3" }, + |description| { vec![] }, + |caps| { vec![] }, + |cached_tail| { vec![] }, + |inputs| { vec![] }, + |op| { + Op::Exec(ExecOp { + mounts: vec![], + network: NetMode::Unset.into(), + security: SecurityMode::Sandbox.into(), + meta: Some(Meta { + args: crate::utils::test::to_vec(vec!["/bin/sh", "-c", "echo arm"]), + cwd: "/".into(), + user: "root".into(), + + ..Default::default() + }), + ..Default::default() + }) + }, + |platform| { + Some(Platform { + os: "linux".into(), + architecture: "arm64".into(), + ..Default::default() + }) + }, + ); +} + #[test] fn serialization_with_ssh_mounts() { use crate::prelude::*; diff --git a/buildkit-llb/src/ops/mod.rs b/buildkit-llb/src/ops/mod.rs index d5bc05f..4d85883 100644 --- a/buildkit-llb/src/ops/mod.rs +++ b/buildkit-llb/src/ops/mod.rs @@ -2,11 +2,13 @@ use std::sync::Arc; pub mod exec; pub mod fs; +pub mod platform; pub mod source; pub mod terminal; pub use self::exec::Command; pub use self::fs::FileSystem; +pub use self::platform::Platform; pub use self::source::Source; pub use self::terminal::Terminal; diff --git a/buildkit-llb/src/ops/platform.rs b/buildkit-llb/src/ops/platform.rs new file mode 100644 index 0000000..66fe473 --- /dev/null +++ b/buildkit-llb/src/ops/platform.rs @@ -0,0 +1,141 @@ +use buildkit_proto::pb; + +/// Platform descriptor as used by BuildKit's `pb::Op.platform`. Re-exported +/// from `buildkit-proto` so callers can construct one directly when they +/// need a knob the helpers below don't expose. +pub use buildkit_proto::pb::Platform; + +/// Constants reused by all helpers. +const LINUX: &str = "linux"; +const WINDOWS: &str = "windows"; +const DARWIN: &str = "darwin"; + +/// `linux/amd64`. +pub fn linux_amd64() -> Platform { + Platform { + os: LINUX.into(), + architecture: "amd64".into(), + ..pb::Platform::default() + } +} + +/// `linux/arm64`. +pub fn linux_arm64() -> Platform { + Platform { + os: LINUX.into(), + architecture: "arm64".into(), + ..pb::Platform::default() + } +} + +/// `linux/arm/v7`. +pub fn linux_arm_v7() -> Platform { + Platform { + os: LINUX.into(), + architecture: "arm".into(), + variant: "v7".into(), + ..pb::Platform::default() + } +} + +/// `linux/arm/v6`. +pub fn linux_arm_v6() -> Platform { + Platform { + os: LINUX.into(), + architecture: "arm".into(), + variant: "v6".into(), + ..pb::Platform::default() + } +} + +/// `linux/386`. +pub fn linux_386() -> Platform { + Platform { + os: LINUX.into(), + architecture: "386".into(), + ..pb::Platform::default() + } +} + +/// `linux/ppc64le`. +pub fn linux_ppc64le() -> Platform { + Platform { + os: LINUX.into(), + architecture: "ppc64le".into(), + ..pb::Platform::default() + } +} + +/// `linux/s390x`. +pub fn linux_s390x() -> Platform { + Platform { + os: LINUX.into(), + architecture: "s390x".into(), + ..pb::Platform::default() + } +} + +/// `linux/riscv64`. +pub fn linux_riscv64() -> Platform { + Platform { + os: LINUX.into(), + architecture: "riscv64".into(), + ..pb::Platform::default() + } +} + +/// `windows/amd64`. +pub fn windows_amd64() -> Platform { + Platform { + os: WINDOWS.into(), + architecture: "amd64".into(), + ..pb::Platform::default() + } +} + +/// `darwin/amd64`. +pub fn darwin_amd64() -> Platform { + Platform { + os: DARWIN.into(), + architecture: "amd64".into(), + ..pb::Platform::default() + } +} + +/// `darwin/arm64`. +pub fn darwin_arm64() -> Platform { + Platform { + os: DARWIN.into(), + architecture: "arm64".into(), + ..pb::Platform::default() + } +} + +/// Canonical platform identifier as used as a key in BuildKit's +/// `RefMap` and as the suffix on the `containerimage.config/` +/// metadata key. Examples: `linux/amd64`, `linux/arm/v7`. +pub fn platform_id(platform: &Platform) -> String { + if platform.variant.is_empty() { + format!("{}/{}", platform.os, platform.architecture) + } else { + format!( + "{}/{}/{}", + platform.os, platform.architecture, platform.variant + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn ids() { + assert_eq!(platform_id(&linux_amd64()), "linux/amd64"); + assert_eq!(platform_id(&linux_arm64()), "linux/arm64"); + assert_eq!(platform_id(&linux_arm_v7()), "linux/arm/v7"); + assert_eq!(platform_id(&linux_arm_v6()), "linux/arm/v6"); + assert_eq!(platform_id(&windows_amd64()), "windows/amd64"); + assert_eq!(platform_id(&darwin_arm64()), "darwin/arm64"); + } +} diff --git a/buildkit-llb/src/ops/source/image.rs b/buildkit-llb/src/ops/source/image.rs index bfa7c22..6acb0e2 100644 --- a/buildkit-llb/src/ops/source/image.rs +++ b/buildkit-llb/src/ops/source/image.rs @@ -6,6 +6,7 @@ use buildkit_proto::pb::{self, op::Op, OpMetadata, SourceOp}; use lazy_static::*; use regex::Regex; +use crate::ops::platform::Platform; use crate::ops::{OperationBuilder, SingleBorrowedOutput, SingleOwnedOutput}; use crate::serialization::{Context, Node, Operation, OperationId, Result}; use crate::utils::{OperationOutput, OutputIdx}; @@ -22,6 +23,7 @@ pub struct ImageSource { description: HashMap, ignore_cache: bool, resolve_mode: Option, + platform: Option, } #[derive(Debug, Clone, Copy, Default)] @@ -115,6 +117,7 @@ impl ImageSource { description: Default::default(), ignore_cache: false, resolve_mode: None, + platform: None, } } @@ -127,6 +130,20 @@ impl ImageSource { self.resolve_mode } + /// Constrain the image to a specific platform. The platform is both + /// recorded on the LLB op (so cross-platform manifest resolution picks + /// the matching layer) and exposed via [`platform()`](Self::platform) + /// for callers that need to pass it to + /// [`Bridge::resolve_image_config`](https://docs.rs/buildkit-frontend). + pub fn with_platform(mut self, platform: Platform) -> Self { + self.platform = Some(platform); + self + } + + pub fn platform(&self) -> Option<&Platform> { + self.platform.as_ref() + } + pub fn with_digest(mut self, digest: S) -> Self where S: Into, @@ -207,6 +224,7 @@ impl Operation for ImageSource { identifier: format!("docker-image://{}", self.canonical_name()), attrs, })), + platform: self.platform.clone(), ..Default::default() }; @@ -285,6 +303,33 @@ fn serialization() { ); } +#[test] +fn serialization_with_platform() { + use crate::ops::platform; + + crate::check_op!( + ImageSource::new("library/alpine:latest").with_platform(platform::linux_arm_v7()), + |description| { vec![] }, + |caps| { vec![] }, + |cached_tail| { vec![] }, + |inputs| { vec![] }, + |op| { + Op::Source(SourceOp { + identifier: "docker-image://docker.io/library/alpine:latest".into(), + attrs: Default::default(), + }) + }, + |platform| { + Some(pb::Platform { + os: "linux".into(), + architecture: "arm".into(), + variant: "v7".into(), + ..Default::default() + }) + }, + ); +} + #[test] fn resolve_mode() { crate::check_op!( diff --git a/buildkit-llb/src/utils.rs b/buildkit-llb/src/utils.rs index eefb607..68eb8f8 100644 --- a/buildkit-llb/src/utils.rs +++ b/buildkit-llb/src/utils.rs @@ -174,6 +174,20 @@ pub mod test { ($serialized:expr, $context:expr, digest, $value:expr) => { assert_eq!($serialized.digest, $value); }; + + ($serialized:expr, $context:expr, platform, $value:expr) => {{ + use std::io::Cursor; + + use buildkit_proto::pb; + use prost::Message; + + assert_eq!( + pb::Op::decode(Cursor::new(&$serialized.bytes)) + .unwrap() + .platform, + $value, + ); + }}; } use std::collections::HashMap; From 45056103fe3329c3d11f3dfe1ddcdb7bb1f9302e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 27 Apr 2026 17:48:16 +0000 Subject: [PATCH 10/21] buildkit-frontend: multi-platform solve, return path and OCI image index Picks up the platform-aware ops added on the buildkit-llb side and wires multi-platform support through the bridge end to end: - `Bridge::resolve_image_config` now forwards `ImageSource::platform()` into `ResolveImageConfigRequest.platform`, so per-platform configs are resolved correctly. - `Bridge::solve_multi_platform[ _with_cache]` accepts a graph that yields a `RefMap` and returns `HashMap` keyed by the canonical platform string. Single-ref responses degrade to a one-entry map. - `FrontendOutput` now has a `with_multi_platform(Vec)` constructor alongside the existing single-output ones. Each entry pairs a `pb::Platform` with an `OutputRef` and an optional `ImageSpecification`; the id defaults to `platform_id(&p)` and can be overridden via `with_id()`. - `Bridge::finish_with_success` now takes the whole `FrontendOutput` and dispatches: single results keep the previous wire format, while multi-platform results are encoded as `RefResult::Refs(RefMap)` plus a `refs.platforms` metadata blob (matching BuildKit's `exptypes.Platforms` JSON shape - capital `ID`, dotted `os.version` / `os.features` keys) plus a per-platform `containerimage.config/` entry whenever `image_spec` is set. Adds OCI types in `oci.rs` for callers that need to consume or build a manifest list directly: `ImageIndex`, `Descriptor`, and an OCI `Platform` that reuses the existing typed `Architecture` / `OperatingSystem` enums and round-trips through the dotted `os.version` / `os.features` JSON keys. Tests: 13 in buildkit-frontend (was 9): the new ones cover the OCI index round-trip, the `refs.platforms` JSON shape, and the duplicate / empty error paths. https://claude.ai/code/session_01XtZHcL6rKJDuX7tUS3okdc --- buildkit-frontend/src/bridge.rs | 285 ++++++++++++++++++++++++++++---- buildkit-frontend/src/lib.rs | 76 ++++++++- buildkit-frontend/src/oci.rs | 104 ++++++++++++ 3 files changed, 424 insertions(+), 41 deletions(-) diff --git a/buildkit-frontend/src/bridge.rs b/buildkit-frontend/src/bridge.rs index 85fbebe..787fcf7 100644 --- a/buildkit-frontend/src/bridge.rs +++ b/buildkit-frontend/src/bridge.rs @@ -4,6 +4,7 @@ use std::sync::Arc; use failure::{bail, format_err, Error, ResultExt}; use log::*; +use serde::Serialize; use tokio::sync::Mutex; use tonic::transport::channel::Channel; @@ -12,9 +13,10 @@ use tonic::Request; use buildkit_proto::google::rpc::Status; use buildkit_proto::moby::buildkit::v1::frontend::llb_bridge_client::LlbBridgeClient; use buildkit_proto::moby::buildkit::v1::frontend::{ - result::Result as RefResult, ReadFileRequest, Ref, ResolveImageConfigRequest, + result::Result as RefResult, ReadFileRequest, Ref, RefMap, ResolveImageConfigRequest, Result as Output, ReturnRequest, SolveRequest, }; +use buildkit_proto::pb; pub use buildkit_llb::ops::source::ImageSource; pub use buildkit_llb::ops::Terminal; @@ -24,6 +26,17 @@ use crate::error::ErrorCode; use crate::oci::ImageSpecification; use crate::options::common::CacheOptionsEntry; use crate::utils::OutputRef; +use crate::{FrontendOutput, FrontendOutputInner, MultiPlatformEntry}; + +/// Metadata key under which the OCI image config (`ImageSpecification`) is +/// returned to BuildKit. For multi-platform results the per-platform +/// id (e.g. `linux/amd64`) is appended after a `/`. +const CONFIG_KEY: &str = "containerimage.config"; + +/// Metadata key under which BuildKit expects the JSON-encoded list of +/// platforms produced by a multi-platform frontend, in the format described +/// by [`buildkit/exporter/exptypes.Platforms`](https://github.com/moby/buildkit/blob/master/exporter/exptypes/types.go). +const PLATFORMS_KEY: &str = "refs.platforms"; #[derive(Clone)] pub struct Bridge { @@ -44,7 +57,7 @@ impl Bridge { ) -> Result<(String, ImageSpecification), Error> { let request = ResolveImageConfigRequest { r#ref: image.canonical_name(), - platform: None, + platform: image.platform().cloned(), resolve_mode: image.resolve_mode().unwrap_or_default().to_string(), log_name: log.unwrap_or_default().into(), @@ -78,6 +91,53 @@ impl Bridge { graph: Terminal<'b>, cache: &[CacheOptionsEntry], ) -> Result { + let inner = self.send_solve(graph, cache).await?; + + match inner { + RefResult::Ref(Ref { id, .. }) => Ok(OutputRef(id)), + other => bail!("Unexpected solve response: {:?}", other), + } + } + + /// Solve a graph that produces multiple per-platform refs and return + /// them keyed by the canonical platform string (`linux/amd64`, + /// `linux/arm/v7`, ...). Use this when the LLB graph itself is + /// already multi-platform aware (e.g. produced by a delegating + /// frontend); to assemble per-platform refs that the current frontend + /// solved separately use [`FrontendOutput::with_multi_platform`]. + pub async fn solve_multi_platform<'a, 'b: 'a>( + &'a self, + graph: Terminal<'b>, + ) -> Result, Error> { + self.solve_multi_platform_with_cache(graph, &[]).await + } + + pub async fn solve_multi_platform_with_cache<'a, 'b: 'a>( + &'a self, + graph: Terminal<'b>, + cache: &[CacheOptionsEntry], + ) -> Result, Error> { + let inner = self.send_solve(graph, cache).await?; + + match inner { + RefResult::Refs(RefMap { refs }) => Ok(refs + .into_iter() + .map(|(id, Ref { id: ref_id, .. })| (id, OutputRef(ref_id))) + .collect()), + RefResult::Ref(Ref { id, .. }) => { + let mut map = HashMap::new(); + map.insert(String::new(), OutputRef(id)); + Ok(map) + } + other => bail!("Unexpected solve response: {:?}", other), + } + } + + async fn send_solve<'a, 'b: 'a>( + &'a self, + graph: Terminal<'b>, + cache: &[CacheOptionsEntry], + ) -> Result { debug!("serializing a graph to request"); let request = SolveRequest { definition: Some(graph.into_definition()), @@ -104,16 +164,9 @@ impl Bridge { debug!("got response: {:#?}", response); - let inner = { - response - .result - .ok_or_else(|| format_err!("Unable to extract solve result"))? - }; - - match inner { - RefResult::Ref(Ref { id, .. }) => Ok(OutputRef(id)), - other => bail!("Unexpected solve response: {:?}", other), - } + response + .result + .ok_or_else(|| format_err!("Unable to extract solve result")) } pub async fn read_file<'a, 'b: 'a, P>( @@ -150,28 +203,30 @@ impl Bridge { Ok(response) } - pub(crate) async fn finish_with_success( - self, - output: OutputRef, - config: Option, - ) -> Result<(), Error> { - let mut metadata = HashMap::new(); - - if let Some(config) = config { - metadata.insert("containerimage.config".into(), serde_json::to_vec(&config)?); - } - - let request = ReturnRequest { - error: None, - result: Some(Output { - result: Some(RefResult::Ref(Ref { - id: output.0, - def: None, - })), - metadata, - - ..Default::default() - }), + pub(crate) async fn finish_with_success(self, output: FrontendOutput) -> Result<(), Error> { + let request = match output.inner { + FrontendOutputInner::Single { output, image_spec } => { + let mut metadata = HashMap::new(); + if let Some(config) = image_spec { + metadata.insert(CONFIG_KEY.into(), serde_json::to_vec(&config)?); + } + ReturnRequest { + error: None, + result: Some(Output { + result: Some(RefResult::Ref(Ref { + id: output.0, + def: None, + })), + metadata, + + ..Default::default() + }), + } + } + + FrontendOutputInner::MultiPlatform(entries) => { + build_multi_platform_return(&entries)? + } }; self.client @@ -210,3 +265,165 @@ impl Bridge { Ok(()) } } + +/// Build a [`ReturnRequest`] that ships per-platform refs along with the +/// `refs.platforms` metadata blob and per-platform +/// `containerimage.config/` entries. +fn build_multi_platform_return(entries: &[MultiPlatformEntry]) -> Result { + if entries.is_empty() { + bail!("multi-platform output must contain at least one entry"); + } + + let mut refs = HashMap::with_capacity(entries.len()); + let mut metadata = HashMap::with_capacity(entries.len() + 1); + + for entry in entries { + if refs.contains_key(&entry.id) { + bail!("duplicate platform id in multi-platform output: {}", entry.id); + } + refs.insert( + entry.id.clone(), + Ref { + id: entry.output.0.clone(), + def: None, + }, + ); + if let Some(config) = entry.image_spec.as_ref() { + metadata.insert( + format!("{}/{}", CONFIG_KEY, entry.id), + serde_json::to_vec(config)?, + ); + } + } + + let platforms_payload = PlatformsJson { + platforms: entries + .iter() + .map(|entry| PlatformEntryJson { + id: &entry.id, + platform: PlatformJson::from(&entry.platform), + }) + .collect(), + }; + metadata.insert( + PLATFORMS_KEY.into(), + serde_json::to_vec(&platforms_payload)?, + ); + + Ok(ReturnRequest { + error: None, + result: Some(Output { + result: Some(RefResult::Refs(RefMap { refs })), + metadata, + + ..Default::default() + }), + }) +} + +#[derive(Serialize)] +struct PlatformsJson<'a> { + platforms: Vec>, +} + +#[derive(Serialize)] +struct PlatformEntryJson<'a> { + #[serde(rename = "ID")] + id: &'a str, + platform: PlatformJson<'a>, +} + +#[derive(Serialize)] +struct PlatformJson<'a> { + architecture: &'a str, + os: &'a str, + + #[serde(skip_serializing_if = "str::is_empty")] + variant: &'a str, + + #[serde(rename = "os.version", skip_serializing_if = "str::is_empty")] + os_version: &'a str, + + #[serde( + rename = "os.features", + skip_serializing_if = "<[String]>::is_empty" + )] + os_features: &'a [String], +} + +impl<'a> From<&'a pb::Platform> for PlatformJson<'a> { + fn from(p: &'a pb::Platform) -> Self { + Self { + architecture: &p.architecture, + os: &p.os, + variant: &p.variant, + os_version: &p.os_version, + os_features: &p.os_features, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::utils::OutputRef; + use buildkit_llb::ops::platform; + + #[test] + fn multi_platform_metadata_shape() { + let entries = vec![ + MultiPlatformEntry::new(platform::linux_amd64(), OutputRef("amd64-id".into())), + MultiPlatformEntry::new(platform::linux_arm_v7(), OutputRef("arm-id".into())), + ]; + + let request = build_multi_platform_return(&entries).unwrap(); + let result = request.result.unwrap(); + + match result.result.unwrap() { + RefResult::Refs(RefMap { refs }) => { + assert_eq!(refs.len(), 2); + assert_eq!(refs.get("linux/amd64").unwrap().id, "amd64-id"); + assert_eq!(refs.get("linux/arm/v7").unwrap().id, "arm-id"); + } + other => panic!("expected RefResult::Refs, got {:?}", other), + } + + let platforms_blob = result.metadata.get(PLATFORMS_KEY).unwrap(); + let parsed: serde_json::Value = serde_json::from_slice(platforms_blob).unwrap(); + let arr = parsed["platforms"].as_array().unwrap(); + assert_eq!(arr.len(), 2); + + let amd = arr.iter().find(|e| e["ID"] == "linux/amd64").unwrap(); + assert_eq!(amd["platform"]["architecture"], "amd64"); + assert_eq!(amd["platform"]["os"], "linux"); + assert!(amd["platform"].get("variant").is_none()); + + let arm = arr.iter().find(|e| e["ID"] == "linux/arm/v7").unwrap(); + assert_eq!(arm["platform"]["architecture"], "arm"); + assert_eq!(arm["platform"]["os"], "linux"); + assert_eq!(arm["platform"]["variant"], "v7"); + + // No image_spec was attached, so no per-platform config keys. + assert!(result + .metadata + .keys() + .all(|k| !k.starts_with(&format!("{}/", CONFIG_KEY)))); + } + + #[test] + fn multi_platform_rejects_duplicate_id() { + let entries = vec![ + MultiPlatformEntry::new(platform::linux_amd64(), OutputRef("a".into())), + MultiPlatformEntry::new(platform::linux_amd64(), OutputRef("b".into())), + ]; + + let err = build_multi_platform_return(&entries).unwrap_err(); + assert!(err.to_string().contains("duplicate platform id")); + } + + #[test] + fn multi_platform_rejects_empty() { + let err = build_multi_platform_return(&[]).unwrap_err(); + assert!(err.to_string().contains("at least one entry")); + } +} diff --git a/buildkit-frontend/src/lib.rs b/buildkit-frontend/src/lib.rs index 25a2587..fe99419 100644 --- a/buildkit-frontend/src/lib.rs +++ b/buildkit-frontend/src/lib.rs @@ -1,6 +1,7 @@ #![deny(warnings)] #![deny(clippy::all)] +use buildkit_proto::pb; use failure::{Error, ResultExt}; use log::*; use serde::de::DeserializeOwned; @@ -31,23 +32,84 @@ where async fn run(self, bridge: Bridge, options: O) -> Result; } +/// Result returned by [`Frontend::run`]. Either a single output ref (the +/// classic single-platform case) or a list of per-platform refs that will +/// be assembled into an image index by BuildKit's image exporter. pub struct FrontendOutput { - output: OutputRef, - image_spec: Option, + pub(crate) inner: FrontendOutputInner, } -impl FrontendOutput { - pub fn with_ref(output: OutputRef) -> Self { +#[allow(clippy::large_enum_variant)] +pub(crate) enum FrontendOutputInner { + Single { + output: OutputRef, + image_spec: Option, + }, + MultiPlatform(Vec), +} + +/// One platform-keyed entry in a multi-platform [`FrontendOutput`]. The +/// `id` is used both as the key in BuildKit's `RefMap` and as the suffix +/// of the `containerimage.config/` metadata key; it defaults to the +/// canonical `/[/]` form derived from `platform`. +pub struct MultiPlatformEntry { + pub id: String, + pub platform: pb::Platform, + pub output: OutputRef, + pub image_spec: Option, +} + +impl MultiPlatformEntry { + pub fn new(platform: pb::Platform, output: OutputRef) -> Self { + let id = buildkit_llb::ops::platform::platform_id(&platform); Self { + id, + platform, output, image_spec: None, } } + pub fn with_spec(mut self, spec: ImageSpecification) -> Self { + self.image_spec = Some(spec); + self + } + + /// Override the auto-derived id. Use this when integrating with a + /// caller that expects a non-canonical key (rare). + pub fn with_id>(mut self, id: S) -> Self { + self.id = id.into(); + self + } +} + +impl FrontendOutput { + pub fn with_ref(output: OutputRef) -> Self { + Self { + inner: FrontendOutputInner::Single { + output, + image_spec: None, + }, + } + } + pub fn with_spec_and_ref(spec: ImageSpecification, output: OutputRef) -> Self { Self { - output, - image_spec: Some(spec), + inner: FrontendOutputInner::Single { + output, + image_spec: Some(spec), + }, + } + } + + /// Build a multi-platform result. Each entry contributes a per-platform + /// ref (under `RefMap`) and, if `image_spec` is set, a per-platform + /// `containerimage.config/` metadata entry. The bridge also emits + /// the `refs.platforms` JSON blob that BuildKit's image exporter uses + /// to assemble the final manifest list / OCI image index. + pub fn with_multi_platform(entries: Vec) -> Self { + Self { + inner: FrontendOutputInner::MultiPlatform(entries), } } } @@ -68,7 +130,7 @@ where match frontend_entrypoint(&bridge, frontend).await { Ok(output) => { bridge - .finish_with_success(output.output, output.image_spec) + .finish_with_success(output) .await .context("Unable to send a success result")?; } diff --git a/buildkit-frontend/src/oci.rs b/buildkit-frontend/src/oci.rs index db78ba7..30831c6 100644 --- a/buildkit-frontend/src/oci.rs +++ b/buildkit-frontend/src/oci.rs @@ -438,3 +438,107 @@ fn min_serialization() { ref_spec ); } + +// https://github.com/opencontainers/image-spec/blob/v1.0.1/image-index.md +// https://github.com/opencontainers/image-spec/blob/v1.0.1/descriptor.md + +/// OCI Image Index - the JSON structure used to describe a multi-platform +/// (a.k.a. manifest list) image. BuildKit's image exporter assembles one +/// of these from the per-platform refs returned by a frontend, but it can +/// also be useful to consume an existing index from disk or to attach one +/// to a return result. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ImageIndex { + pub schema_version: u32, + + #[serde(skip_serializing_if = "Option::is_none")] + pub media_type: Option, + + pub manifests: Vec, + + #[serde(skip_serializing_if = "Option::is_none")] + pub annotations: Option>, +} + +/// OCI content descriptor pointing at a per-platform manifest inside an +/// [`ImageIndex`]. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Descriptor { + pub media_type: String, + pub digest: String, + pub size: u64, + + #[serde(skip_serializing_if = "Option::is_none")] + pub platform: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub annotations: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub urls: Option>, +} + +/// OCI Platform descriptor (per-manifest target). Mirrors the JSON shape +/// of `ocispecs.Platform` from the image-spec, including the dotted +/// `os.version` / `os.features` keys, so the round-trip with BuildKit's +/// `refs.platforms` metadata stays stable. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Platform { + pub architecture: Architecture, + pub os: OperatingSystem, + + #[serde(skip_serializing_if = "Option::is_none")] + pub variant: Option, + + #[serde(rename = "os.version", skip_serializing_if = "Option::is_none")] + pub os_version: Option, + + #[serde(rename = "os.features", skip_serializing_if = "Option::is_none")] + pub os_features: Option>, +} + +#[test] +fn image_index_roundtrip() { + let json = r#"{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", + "size": 7143, + "platform": { + "architecture": "amd64", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270", + "size": 7682, + "platform": { + "architecture": "arm", + "os": "linux", + "variant": "v7" + } + } + ] +}"#; + + let parsed: ImageIndex = serde_json::from_str(json).unwrap(); + assert_eq!(parsed.schema_version, 2); + assert_eq!(parsed.manifests.len(), 2); + assert_eq!( + parsed.manifests[0].platform.as_ref().unwrap().architecture, + Architecture::Amd64 + ); + assert_eq!( + parsed.manifests[1].platform.as_ref().unwrap().variant, + Some("v7".into()) + ); + + // Re-serializing produces the same JSON. + assert_eq!(serde_json::to_string_pretty(&parsed).unwrap(), json); +} From 6ce31ded437dd1e6ad7e7ad745fc03ebef11a9c2 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 28 Apr 2026 06:43:01 +0000 Subject: [PATCH 11/21] buildkit-frontend: extend oci API with all current OCI/Docker fields The OCI image-spec has gained a handful of optional fields since v1.0 (`os.version`, `os.features`, `variant`, OCI v1.1 referrers, ...) and Docker has long shipped its own widely-used config extensions (`Healthcheck`, `Shell`, `ArgsEscaped`). buildkit-frontend was only exposing the v1.0 base, so any caller round-tripping an existing image config silently dropped these fields. This adds them. Top-level `ImageSpecification`: - `os_version: Option` (JSON `os.version`) - `os_features: Option>` (JSON `os.features`) - `variant: Option` `ImageConfig` now also carries the Docker extensions, all `Option<_>` so the existing OCI-only round-trips stay byte-for-byte stable: - `healthcheck: Option` - new struct mirroring Docker's shape, with `Option` fields (de)serialized as Go's `time.Duration` JSON convention (integer count of nanoseconds). Includes the modern `start_interval` field from newer Docker/containerd. `Test` covers `["NONE"]`, `["CMD", ...]` and `["CMD-SHELL", ...]` shapes. - `shell: Option>` - the default shell used by the shell-form of `RUN` / `CMD` / `ENTRYPOINT`. - `args_escaped: Option` - Windows-only, deprecated but still emitted by older pipelines so we keep it for fidelity. `ImageConfig` now also derives `Default`, so callers can pick the fields they care about with `..Default::default()` instead of having to spell every `None` (the examples are migrated to that style). `Architecture` gains `Riscv64`, `Loong64` and `Wasm` to match the modern Go/OCI list. `OperatingSystem` gains `Aix`, `Android`, `Hurd`, `Illumos`, `Ios`, `Js` and `Zos`. OCI v1.1 additions on the descriptor types: - `Descriptor.artifact_type: Option` - `Descriptor.data: Option` (base64 inline payload) - `ImageIndex.subject: Option` (referrers API) - `ImageIndex.artifact_type: Option` - new `ImageManifest` struct (per-platform manifest, the type that `ImageIndex` entries resolve to) with the same `subject` / `artifactType` knobs. Eight new round-trip tests cover: every Docker-extension field at once, the dotted top-level keys with all three new optionals, `Healthcheck::Test = ["NONE"]` minimal case, the new architecture and OS variants, OCI v1.1 fields on `ImageIndex` and `ImageManifest`, and the empty `ImageConfig::default()`. `cargo test --workspace` now runs 44 tests (was 36); `cargo clippy --workspace --all-targets -- -D warnings` is clean. https://claude.ai/code/session_01XtZHcL6rKJDuX7tUS3okdc --- buildkit-frontend/examples/download.rs | 11 +- buildkit-frontend/examples/reverse.rs | 13 +- buildkit-frontend/examples/ssh-mount.rs | 12 +- buildkit-frontend/src/oci.rs | 428 +++++++++++++++++++++++- 4 files changed, 438 insertions(+), 26 deletions(-) diff --git a/buildkit-frontend/examples/download.rs b/buildkit-frontend/examples/download.rs index c147068..87f7f76 100644 --- a/buildkit-frontend/examples/download.rs +++ b/buildkit-frontend/examples/download.rs @@ -59,18 +59,15 @@ impl DownloadFrontend { architecture: Architecture::Amd64, os: OperatingSystem::Linux, + os_version: None, + os_features: None, + variant: None, config: Some(ImageConfig { entrypoint: Some(vec!["/bin/sh".into()]), cmd: Some(vec!["-c".into(), "/usr/bin/sha256sum *".into()]), - env: None, - user: None, working_dir: Some(OUTPUT_DIR.into()), - - labels: None, - volumes: None, - exposed_ports: None, - stop_signal: None, + ..Default::default() }), rootfs: None, diff --git a/buildkit-frontend/examples/reverse.rs b/buildkit-frontend/examples/reverse.rs index e8393bc..10cf3c3 100644 --- a/buildkit-frontend/examples/reverse.rs +++ b/buildkit-frontend/examples/reverse.rs @@ -40,18 +40,13 @@ impl ReverseFrontend { architecture: Architecture::Amd64, os: OperatingSystem::Linux, + os_version: None, + os_features: None, + variant: None, config: Some(ImageConfig { - entrypoint: None, cmd: Some(vec!["/bin/cat".into(), OUTPUT_FILENAME.into()]), - env: None, - user: None, - working_dir: None, - - labels: None, - volumes: None, - exposed_ports: None, - stop_signal: None, + ..Default::default() }), rootfs: None, diff --git a/buildkit-frontend/examples/ssh-mount.rs b/buildkit-frontend/examples/ssh-mount.rs index 0df8e07..99cf9ba 100644 --- a/buildkit-frontend/examples/ssh-mount.rs +++ b/buildkit-frontend/examples/ssh-mount.rs @@ -40,18 +40,14 @@ impl ReverseFrontend { architecture: Architecture::Amd64, os: OperatingSystem::Linux, + os_version: None, + os_features: None, + variant: None, config: Some(ImageConfig { - entrypoint: None, cmd: Some(vec!["/bin/cat".into(), OUTPUT_FILENAME.into()]), - env: None, - user: None, working_dir: Some("/output".into()), - - labels: None, - volumes: None, - exposed_ports: None, - stop_signal: None, + ..Default::default() }), rootfs: None, diff --git a/buildkit-frontend/src/oci.rs b/buildkit-frontend/src/oci.rs index 30831c6..326b0c8 100644 --- a/buildkit-frontend/src/oci.rs +++ b/buildkit-frontend/src/oci.rs @@ -1,11 +1,42 @@ use std::collections::BTreeMap; use std::convert::TryFrom; use std::path::PathBuf; +use std::time::Duration; use chrono::prelude::*; use serde::{Deserialize, Serialize}; use serde_json::Value; +/// (De)serializer for `Option` fields that follow Go's +/// `time.Duration` JSON convention (integer count of nanoseconds). Used +/// by the Docker [`Healthcheck`] extension where buildkitd, dockerd and +/// containerd all read/write durations as nanos. +mod opt_duration_nanos { + use serde::{Deserialize, Deserializer, Serializer}; + use std::convert::TryFrom; + use std::time::Duration; + + pub fn serialize(d: &Option, s: S) -> Result + where + S: Serializer, + { + match d { + // `skip_serializing_if = "Option::is_none"` filters None + // before this is reached, but handle it defensively. + Some(d) => s.serialize_u64(u64::try_from(d.as_nanos()).unwrap_or(u64::MAX)), + None => s.serialize_none(), + } + } + + pub fn deserialize<'de, D>(d: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + let n: Option = Option::deserialize(d)?; + Ok(n.map(Duration::from_nanos)) + } +} + // https://github.com/opencontainers/image-spec/blob/v1.0.1/config.md #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -24,6 +55,24 @@ pub struct ImageSpecification { /// The name of the operating system which the image is built to run on. pub os: OperatingSystem, + /// Optional version of the operating system, used to differentiate + /// between Windows builds (`10.0.17763.1234`, ...) or specific kernel + /// constraints. Serialized under the dotted JSON key `os.version` + /// per the OCI image-spec. + #[serde(rename = "os.version", skip_serializing_if = "Option::is_none")] + pub os_version: Option, + + /// Optional list of OS features required by the image (Windows uses + /// `win32k` for example). Serialized under the dotted JSON key + /// `os.features` per the OCI image-spec. + #[serde(rename = "os.features", skip_serializing_if = "Option::is_none")] + pub os_features: Option>, + + /// CPU sub-architecture / variant (`v7`, `v8`, ...). Useful to + /// differentiate `linux/arm/v6` from `linux/arm/v7` configs. + #[serde(skip_serializing_if = "Option::is_none")] + pub variant: Option, + /// The execution parameters which should be used as a base when running a container using the image. /// This field can be `None`, in which case any execution parameters should be specified at creation of the container. #[serde(skip_serializing_if = "Option::is_none")] @@ -73,23 +122,39 @@ pub enum Architecture { /// IBM System z 64-bit, big-endian S390x, + + /// 64-bit RISC-V + Riscv64, + + /// 64-bit LoongArch + Loong64, + + /// WebAssembly + Wasm, } #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum OperatingSystem { + Aix, + Android, Darwin, Dragonfly, Freebsd, + Hurd, + Illumos, + Ios, + Js, Linux, Netbsd, Openbsd, Plan9, Solaris, Windows, + Zos, } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(from = "RawImageConfig")] #[serde(into = "RawImageConfig")] pub struct ImageConfig { @@ -119,6 +184,22 @@ pub struct ImageConfig { /// The field contains the system call signal that will be sent to the container to exit. pub stop_signal: Option, + + /// Docker-extension healthcheck definition (`HEALTHCHECK` directive in + /// a Dockerfile). Read by `docker run`, `podman run`, BuildKit and + /// the OCI runtime; absent for OCI-only images that don't opt in. + pub healthcheck: Option, + + /// Docker-extension default shell used by the shell-form of `RUN`, + /// `CMD` and `ENTRYPOINT`. Defaults to `["/bin/sh", "-c"]` on Linux + /// and `["cmd", "/S", "/C"]` on Windows when omitted. + pub shell: Option>, + + /// Windows-only Docker-extension flag indicating that command + /// arguments are already escaped for `cmd.exe` and should not be + /// re-escaped. Deprecated by Docker but still emitted by older + /// pipelines, so kept here for round-trip fidelity. + pub args_escaped: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -150,6 +231,71 @@ struct RawImageConfig { #[serde(skip_serializing_if = "Option::is_none")] stop_signal: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + healthcheck: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + shell: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] + args_escaped: Option, +} + +/// Docker `HEALTHCHECK` payload as carried inside the OCI image config. +/// All durations follow Go's `time.Duration` JSON convention - an +/// integer count of nanoseconds - which is what dockerd, BuildKit and +/// containerd all read and write. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub struct Healthcheck { + /// The probe to run. Conventional shapes: + /// - `["NONE"]` to disable an inherited healthcheck; + /// - `["CMD", "", ...]` to exec the args directly; + /// - `["CMD-SHELL", ""]` to run inside the image's + /// default shell. + #[serde(skip_serializing_if = "Option::is_none")] + pub test: Option>, + + /// Time between the end of one check and the start of the next. + #[serde( + with = "opt_duration_nanos", + skip_serializing_if = "Option::is_none", + default + )] + pub interval: Option, + + /// Maximum time a single probe can take before being considered to + /// have failed. + #[serde( + with = "opt_duration_nanos", + skip_serializing_if = "Option::is_none", + default + )] + pub timeout: Option, + + /// Initial grace period during which probe failures don't count + /// against `retries`. Useful for slow-starting services. + #[serde( + with = "opt_duration_nanos", + skip_serializing_if = "Option::is_none", + default + )] + pub start_period: Option, + + /// During the start period, run probes this often (defaults to + /// `interval` if unset). Added in newer Docker / containerd. + #[serde( + with = "opt_duration_nanos", + skip_serializing_if = "Option::is_none", + default + )] + pub start_interval: Option, + + /// Number of consecutive probe failures required to mark the + /// container unhealthy. + #[serde(skip_serializing_if = "Option::is_none")] + pub retries: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -271,6 +417,9 @@ impl From for ImageConfig { working_dir: raw.working_dir, labels: raw.labels, stop_signal: raw.stop_signal, + healthcheck: raw.healthcheck, + shell: raw.shell, + args_escaped: raw.args_escaped, env: raw.env.map(|inner| { inner @@ -309,6 +458,9 @@ impl From for RawImageConfig { working_dir: val.working_dir, labels: val.labels, stop_signal: val.stop_signal, + healthcheck: val.healthcheck, + shell: val.shell, + args_escaped: val.args_escaped, env: val.env.map(|inner| { inner @@ -344,6 +496,9 @@ fn serialization() { author: Some("Alyssa P. Hacker ".into()), architecture: Architecture::Amd64, os: OperatingSystem::Linux, + os_version: None, + os_features: None, + variant: None, rootfs: Some(ImageRootfs { diff_type: RootfsType::Layers, diff_ids: vec![ @@ -399,6 +554,9 @@ fn serialization() { .collect(), ), stop_signal: Some(Signal::SIGKILL), + healthcheck: None, + shell: None, + args_escaped: None, }), }; @@ -420,6 +578,9 @@ fn min_serialization() { architecture: Architecture::Amd64, os: OperatingSystem::Linux, + os_version: None, + os_features: None, + variant: None, rootfs: Some(ImageRootfs { diff_type: RootfsType::Layers, diff_ids: vec![ @@ -455,14 +616,55 @@ pub struct ImageIndex { #[serde(skip_serializing_if = "Option::is_none")] pub media_type: Option, + /// OCI v1.1 - declares this index as an artifact rather than a + /// regular image, e.g. `application/vnd.in-toto+json` for an + /// attestation index. + #[serde(skip_serializing_if = "Option::is_none")] + pub artifact_type: Option, + pub manifests: Vec, + /// OCI v1.1 - reference to another descriptor this index is "about", + /// used by the referrers API to attach attestations / SBOMs to an + /// existing image. + #[serde(skip_serializing_if = "Option::is_none")] + pub subject: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub annotations: Option>, +} + +/// OCI image manifest (per-platform - what an [`ImageIndex`] entry +/// resolves to). Carries the descriptors of the image config blob and +/// the layer blobs. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ImageManifest { + pub schema_version: u32, + + #[serde(skip_serializing_if = "Option::is_none")] + pub media_type: Option, + + /// OCI v1.1 - declares the manifest as an artifact rather than a + /// regular image (used for SBOMs, attestations, ...). + #[serde(skip_serializing_if = "Option::is_none")] + pub artifact_type: Option, + + pub config: Descriptor, + pub layers: Vec, + + /// OCI v1.1 - reference to another descriptor this manifest is + /// "about" (referrers API). + #[serde(skip_serializing_if = "Option::is_none")] + pub subject: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub annotations: Option>, } /// OCI content descriptor pointing at a per-platform manifest inside an -/// [`ImageIndex`]. +/// [`ImageIndex`], or at the config / layer blobs inside an +/// [`ImageManifest`]. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Descriptor { @@ -478,6 +680,14 @@ pub struct Descriptor { #[serde(skip_serializing_if = "Option::is_none")] pub urls: Option>, + + /// OCI v1.1 - inline blob payload, base64-encoded. + #[serde(skip_serializing_if = "Option::is_none")] + pub data: Option, + + /// OCI v1.1 - declares the referenced blob as an artifact type. + #[serde(skip_serializing_if = "Option::is_none")] + pub artifact_type: Option, } /// OCI Platform descriptor (per-manifest target). Mirrors the JSON shape @@ -542,3 +752,217 @@ fn image_index_roundtrip() { // Re-serializing produces the same JSON. assert_eq!(serde_json::to_string_pretty(&parsed).unwrap(), json); } + +#[cfg(test)] +mod modern_fields_tests { + use super::*; + use std::time::Duration; + + /// Image config carrying every Docker extension we care about + /// (healthcheck, shell, args_escaped) on top of the OCI base. + #[test] + fn image_config_with_healthcheck_shell_args_escaped() { + let json = r#"{ + "Cmd": [ + "nginx", + "-g", + "daemon off;" + ], + "Healthcheck": { + "Test": [ + "CMD-SHELL", + "curl -f http://localhost/ || exit 1" + ], + "Interval": 30000000000, + "Timeout": 5000000000, + "StartPeriod": 60000000000, + "StartInterval": 1000000000, + "Retries": 3 + }, + "Shell": [ + "/bin/bash", + "-c" + ], + "ArgsEscaped": true +}"#; + + let parsed: ImageConfig = serde_json::from_str(json).unwrap(); + let hc = parsed.healthcheck.as_ref().unwrap(); + assert_eq!( + hc.test.as_ref().unwrap(), + &vec![ + "CMD-SHELL".to_string(), + "curl -f http://localhost/ || exit 1".to_string(), + ] + ); + assert_eq!(hc.interval, Some(Duration::from_secs(30))); + assert_eq!(hc.timeout, Some(Duration::from_secs(5))); + assert_eq!(hc.start_period, Some(Duration::from_secs(60))); + assert_eq!(hc.start_interval, Some(Duration::from_secs(1))); + assert_eq!(hc.retries, Some(3)); + assert_eq!(parsed.shell.as_deref(), Some(&["/bin/bash".into(), "-c".into()][..])); + assert_eq!(parsed.args_escaped, Some(true)); + + // Round-trip back to the same JSON. + assert_eq!(serde_json::to_string_pretty(&parsed).unwrap(), json); + } + + /// `ImageSpecification` with the OCI v1.0.2+ top-level optional fields + /// (`variant`, `os.version`, `os.features`). + #[test] + fn image_spec_with_variant_and_os_version() { + let json = r#"{ + "architecture": "arm", + "os": "linux", + "os.version": "5.10", + "os.features": [ + "vfp", + "neon" + ], + "variant": "v7" +}"#; + + let parsed: ImageSpecification = serde_json::from_str(json).unwrap(); + assert_eq!(parsed.architecture, Architecture::ARM); + assert_eq!(parsed.variant.as_deref(), Some("v7")); + assert_eq!(parsed.os_version.as_deref(), Some("5.10")); + assert_eq!( + parsed.os_features.as_ref().unwrap(), + &vec!["vfp".to_string(), "neon".to_string()] + ); + + assert_eq!(serde_json::to_string_pretty(&parsed).unwrap(), json); + } + + /// New `Architecture` enum members survive a round-trip. + #[test] + fn arch_riscv_wasm_loong() { + for (arch, wire) in [ + (Architecture::Riscv64, "riscv64"), + (Architecture::Wasm, "wasm"), + (Architecture::Loong64, "loong64"), + ] { + let json = serde_json::to_string(&arch).unwrap(); + assert_eq!(json, format!("\"{}\"", wire)); + let back: Architecture = serde_json::from_str(&json).unwrap(); + assert_eq!(back, arch); + } + } + + /// New `OperatingSystem` enum members survive a round-trip. + #[test] + fn os_modern_variants() { + for (os, wire) in [ + (OperatingSystem::Aix, "aix"), + (OperatingSystem::Android, "android"), + (OperatingSystem::Illumos, "illumos"), + (OperatingSystem::Ios, "ios"), + (OperatingSystem::Js, "js"), + (OperatingSystem::Zos, "zos"), + ] { + let json = serde_json::to_string(&os).unwrap(); + assert_eq!(json, format!("\"{}\"", wire)); + let back: OperatingSystem = serde_json::from_str(&json).unwrap(); + assert_eq!(back, os); + } + } + + /// `ImageManifest` round-trips a minimal-but-realistic OCI v1.1 + /// manifest with `subject`, `artifactType` and inline blob `data`. + #[test] + fn image_manifest_with_oci_v1_1_fields() { + let json = r#"{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "artifactType": "application/vnd.example.sbom.v1+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7", + "size": 1234 + }, + "layers": [ + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "digest": "sha256:9834876dcfb05cb167a5c24953eba58c4ac89b1adf57f28f2f9d09af107ee8f0", + "size": 32654, + "data": "aGVsbG8=" + } + ], + "subject": { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270", + "size": 7682 + } +}"#; + + let parsed: ImageManifest = serde_json::from_str(json).unwrap(); + assert_eq!(parsed.schema_version, 2); + assert_eq!( + parsed.artifact_type.as_deref(), + Some("application/vnd.example.sbom.v1+json") + ); + assert_eq!(parsed.layers.len(), 1); + assert_eq!(parsed.layers[0].data.as_deref(), Some("aGVsbG8=")); + assert!(parsed.subject.is_some()); + + assert_eq!(serde_json::to_string_pretty(&parsed).unwrap(), json); + } + + /// `ImageIndex` round-trips with `subject` (referrers API) and + /// `artifactType` (OCI v1.1). + #[test] + fn image_index_with_oci_v1_1_fields() { + let json = r#"{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.index.v1+json", + "artifactType": "application/vnd.example.attestation+json", + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", + "size": 7143 + } + ], + "subject": { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270", + "size": 7682 + } +}"#; + + let parsed: ImageIndex = serde_json::from_str(json).unwrap(); + assert_eq!( + parsed.artifact_type.as_deref(), + Some("application/vnd.example.attestation+json") + ); + assert!(parsed.subject.is_some()); + + assert_eq!(serde_json::to_string_pretty(&parsed).unwrap(), json); + } + + /// Healthcheck `Test: ["NONE"]` (disable inherited healthcheck) with + /// no other fields set is a common Dockerfile case. + #[test] + fn healthcheck_none_only_serializes_test() { + let hc = Healthcheck { + test: Some(vec!["NONE".into()]), + interval: None, + timeout: None, + start_period: None, + start_interval: None, + retries: None, + }; + assert_eq!( + serde_json::to_string(&hc).unwrap(), + r#"{"Test":["NONE"]}"# + ); + } + + /// `ImageConfig::default()` is empty - useful so callers can populate + /// only the fields they care about via `..Default::default()`. + #[test] + fn image_config_default_is_empty() { + let cfg = ImageConfig::default(); + assert_eq!(serde_json::to_string(&cfg).unwrap(), "{}"); + } +} From 6cbf0788379b56466dd056e330900a355e8ea826 Mon Sep 17 00:00:00 2001 From: Thomas DA ROCHA Date: Tue, 28 Apr 2026 18:17:12 +0200 Subject: [PATCH 12/21] feat: Platform serialization --- buildkit-llb/src/ops/platform.rs | 9 +------ buildkit-proto/src/lib.rs | 2 ++ buildkit-proto/src/serialization/mod.rs | 1 + buildkit-proto/src/serialization/platform.rs | 27 ++++++++++++++++++++ 4 files changed, 31 insertions(+), 8 deletions(-) create mode 100644 buildkit-proto/src/serialization/mod.rs create mode 100644 buildkit-proto/src/serialization/platform.rs diff --git a/buildkit-llb/src/ops/platform.rs b/buildkit-llb/src/ops/platform.rs index 66fe473..dd20018 100644 --- a/buildkit-llb/src/ops/platform.rs +++ b/buildkit-llb/src/ops/platform.rs @@ -115,14 +115,7 @@ pub fn darwin_arm64() -> Platform { /// `RefMap` and as the suffix on the `containerimage.config/` /// metadata key. Examples: `linux/amd64`, `linux/arm/v7`. pub fn platform_id(platform: &Platform) -> String { - if platform.variant.is_empty() { - format!("{}/{}", platform.os, platform.architecture) - } else { - format!( - "{}/{}/{}", - platform.os, platform.architecture, platform.variant - ) - } + platform.to_string() } #[cfg(test)] diff --git a/buildkit-proto/src/lib.rs b/buildkit-proto/src/lib.rs index 7485b6c..d3ad1b6 100644 --- a/buildkit-proto/src/lib.rs +++ b/buildkit-proto/src/lib.rs @@ -1,3 +1,5 @@ +mod serialization; + #[allow(clippy::all)] pub mod moby { diff --git a/buildkit-proto/src/serialization/mod.rs b/buildkit-proto/src/serialization/mod.rs new file mode 100644 index 0000000..46ba91e --- /dev/null +++ b/buildkit-proto/src/serialization/mod.rs @@ -0,0 +1 @@ +mod platform; \ No newline at end of file diff --git a/buildkit-proto/src/serialization/platform.rs b/buildkit-proto/src/serialization/platform.rs new file mode 100644 index 0000000..1c6e1d6 --- /dev/null +++ b/buildkit-proto/src/serialization/platform.rs @@ -0,0 +1,27 @@ +use std::str::FromStr; + +use crate::pb::Platform; + +impl FromStr for Platform { + type Err = String; + + fn from_str(s: &str) -> Result { + let parts: Vec<&str> = s.splitn(3, '/').collect(); + Ok(Platform { + os: parts.first().copied().unwrap_or("linux").to_string(), + architecture: parts.get(1).copied().unwrap_or("amd64").to_string(), + variant: parts.get(2).copied().unwrap_or("").to_string(), + ..Platform::default() + }) + } +} + +impl ToString for Platform { + fn to_string(&self) -> String { + if self.variant.is_empty() { + format!("{}/{}", self.os, self.architecture) + } else { + format!("{}/{}/{}", self.os, self.architecture, self.variant) + } + } +} From 614d461cb1b446fb765fe3a6131ea8487b0ff1a2 Mon Sep 17 00:00:00 2001 From: Thomas DA ROCHA Date: Wed, 20 May 2026 15:29:23 +0200 Subject: [PATCH 13/21] fix: Add missing proto --- .ci/test-reverse-example.sh | 5 +- buildkit-frontend/examples/reverse.input | 2 +- .../proto/google/protobuf/timestamp.proto | 145 ++++++++++++++++++ buildkit-proto/update.sh | 1 + 4 files changed, 150 insertions(+), 3 deletions(-) create mode 100644 buildkit-proto/proto/google/protobuf/timestamp.proto diff --git a/.ci/test-reverse-example.sh b/.ci/test-reverse-example.sh index ef86cdf..a6da57f 100755 --- a/.ci/test-reverse-example.sh +++ b/.ci/test-reverse-example.sh @@ -1,12 +1,13 @@ #!/bin/bash source $(dirname $0)/common.sh -FRONTEND_LABEL="rust-buildkit:reverse-frontend" -OUTPUT_LABEL="rust-buildkit:reverse-image" +FRONTEND_LABEL="localhost:5000/rust-buildkit:reverse-frontend" +OUTPUT_LABEL="localhost:5000/rust-buildkit:reverse-image" set -ex docker build -t $FRONTEND_LABEL -f $EXAMPLES_DIR/reverse.dockerfile $WORKSPACE_DIR +docker push $FRONTEND_LABEL docker build -t $OUTPUT_LABEL -f $EXAMPLES_DIR/reverse.input $WORKSPACE_DIR diff --strip-trailing-cr --color=always <(cat $EXAMPLES_DIR/reverse.output) <(docker run --rm $OUTPUT_LABEL) diff --git a/buildkit-frontend/examples/reverse.input b/buildkit-frontend/examples/reverse.input index adb059f..8ffa1cc 100644 --- a/buildkit-frontend/examples/reverse.input +++ b/buildkit-frontend/examples/reverse.input @@ -1,4 +1,4 @@ -# syntax = rust-buildkit:reverse-frontend +# syntax = localhost:5000/rust-buildkit:reverse-frontend Hello from the reverse example "dockerfile". Every line from this file has to be printed in reverse when you run the resulting image. diff --git a/buildkit-proto/proto/google/protobuf/timestamp.proto b/buildkit-proto/proto/google/protobuf/timestamp.proto new file mode 100644 index 0000000..6bc1efc --- /dev/null +++ b/buildkit-proto/proto/google/protobuf/timestamp.proto @@ -0,0 +1,145 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +syntax = "proto3"; + +package google.protobuf; + +option cc_enable_arenas = true; +option go_package = "google.golang.org/protobuf/types/known/timestamppb"; +option java_package = "com.google.protobuf"; +option java_outer_classname = "TimestampProto"; +option java_multiple_files = true; +option objc_class_prefix = "GPB"; +option csharp_namespace = "Google.Protobuf.WellKnownTypes"; + +// A Timestamp represents a point in time independent of any time zone or local +// calendar, encoded as a count of seconds and fractions of seconds at +// nanosecond resolution. The count is relative to an epoch at UTC midnight on +// January 1, 1970, in the proleptic Gregorian calendar which extends the +// Gregorian calendar backwards to year one. +// +// All minutes are 60 seconds long. Leap seconds are "smeared" so that no leap +// second table is needed for interpretation, using a [24-hour linear +// smear](https://developers.google.com/time/smear). +// +// The range is from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. By +// restricting to that range, we ensure that we can convert to and from [RFC +// 3339](https://www.ietf.org/rfc/rfc3339.txt) date strings. +// +// # Examples +// +// Example 1: Compute Timestamp from POSIX `time()`. +// +// Timestamp timestamp; +// timestamp.set_seconds(time(NULL)); +// timestamp.set_nanos(0); +// +// Example 2: Compute Timestamp from POSIX `gettimeofday()`. +// +// struct timeval tv; +// gettimeofday(&tv, NULL); +// +// Timestamp timestamp; +// timestamp.set_seconds(tv.tv_sec); +// timestamp.set_nanos(tv.tv_usec * 1000); +// +// Example 3: Compute Timestamp from Win32 `GetSystemTimeAsFileTime()`. +// +// FILETIME ft; +// GetSystemTimeAsFileTime(&ft); +// UINT64 ticks = (((UINT64)ft.dwHighDateTime) << 32) | ft.dwLowDateTime; +// +// // A Windows tick is 100 nanoseconds. Windows epoch 1601-01-01T00:00:00Z +// // is 11644473600 seconds before Unix epoch 1970-01-01T00:00:00Z. +// Timestamp timestamp; +// timestamp.set_seconds((INT64) ((ticks / 10000000) - 11644473600LL)); +// timestamp.set_nanos((INT32) ((ticks % 10000000) * 100)); +// +// Example 4: Compute Timestamp from Java `System.currentTimeMillis()`. +// +// long millis = System.currentTimeMillis(); +// +// Timestamp timestamp = Timestamp.newBuilder().setSeconds(millis / 1000) +// .setNanos((int) ((millis % 1000) * 1000000)).build(); +// +// Example 5: Compute Timestamp from Java `Instant.now()`. +// +// Instant now = Instant.now(); +// +// Timestamp timestamp = +// Timestamp.newBuilder().setSeconds(now.getEpochSecond()) +// .setNanos(now.getNano()).build(); +// +// Example 6: Compute Timestamp from current time in Python. +// +// timestamp = Timestamp() +// timestamp.GetCurrentTime() +// +// # JSON Mapping +// +// In JSON format, the Timestamp type is encoded as a string in the +// [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format. That is, the +// format is "{year}-{month}-{day}T{hour}:{min}:{sec}[.{frac_sec}]Z" +// where {year} is always expressed using four digits while {month}, {day}, +// {hour}, {min}, and {sec} are zero-padded to two digits each. The fractional +// seconds, which can go up to 9 digits (i.e. up to 1 nanosecond resolution), +// are optional. The "Z" suffix indicates the timezone ("UTC"); the timezone +// is required. A ProtoJSON serializer should always use UTC (as indicated by +// "Z") when printing the Timestamp type and a ProtoJSON parser should be +// able to accept both UTC and other timezones (as indicated by an offset). +// +// For example, "2017-01-15T01:30:15.01Z" encodes 15.01 seconds past +// 01:30 UTC on January 15, 2017. +// +// In JavaScript, one can convert a Date object to this format using the +// standard +// [toISOString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString) +// method. In Python, a standard `datetime.datetime` object can be converted +// to this format using +// [`strftime`](https://docs.python.org/2/library/time.html#time.strftime) with +// the time format spec '%Y-%m-%dT%H:%M:%S.%fZ'. Likewise, in Java, one can use +// the Joda Time's [`ISODateTimeFormat.dateTime()`]( +// http://joda-time.sourceforge.net/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTime() +// ) to obtain a formatter capable of generating timestamps in this format. +// +message Timestamp { + // Represents seconds of UTC time since Unix epoch 1970-01-01T00:00:00Z. Must + // be between -62135596800 and 253402300799 inclusive (which corresponds to + // 0001-01-01T00:00:00Z to 9999-12-31T23:59:59Z). + int64 seconds = 1; + + // Non-negative fractions of a second at nanosecond resolution. This field is + // the nanosecond portion of the duration, not an alternative to seconds. + // Negative second values with fractions must still have non-negative nanos + // values that count forward in time. Must be between 0 and 999,999,999 + // inclusive. + int32 nanos = 2; +} diff --git a/buildkit-proto/update.sh b/buildkit-proto/update.sh index 4b36e00..6627ca8 100755 --- a/buildkit-proto/update.sh +++ b/buildkit-proto/update.sh @@ -34,3 +34,4 @@ curl "https://raw.githubusercontent.com/planetscale/vtprotobuf/refs/heads/main/i # Download protobuf standard files curl "https://raw.githubusercontent.com/protocolbuffers/protobuf/main/src/google/protobuf/descriptor.proto" > proto/google/protobuf/descriptor.proto curl "https://raw.githubusercontent.com/protocolbuffers/protobuf/main/src/google/protobuf/any.proto" > proto/google/protobuf/any.proto +curl "https://raw.githubusercontent.com/protocolbuffers/protobuf/main/src/google/protobuf/timestamp.proto" > proto/google/protobuf/timestamp.proto From 7e07620fbd990b2550374a670bbc2553fe417037 Mon Sep 17 00:00:00 2001 From: Thomas DA ROCHA Date: Wed, 20 May 2026 17:35:23 +0200 Subject: [PATCH 14/21] fix: Force exit --- buildkit-frontend/src/bridge.rs | 4 ++-- buildkit-frontend/src/lib.rs | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/buildkit-frontend/src/bridge.rs b/buildkit-frontend/src/bridge.rs index 787fcf7..d39897d 100644 --- a/buildkit-frontend/src/bridge.rs +++ b/buildkit-frontend/src/bridge.rs @@ -94,7 +94,7 @@ impl Bridge { let inner = self.send_solve(graph, cache).await?; match inner { - RefResult::Ref(Ref { id, .. }) => Ok(OutputRef(id)), + RefResult::Ref(Ref { id, .. }) | RefResult::RefDeprecated(id) => Ok(OutputRef(id)), other => bail!("Unexpected solve response: {:?}", other), } } @@ -124,7 +124,7 @@ impl Bridge { .into_iter() .map(|(id, Ref { id: ref_id, .. })| (id, OutputRef(ref_id))) .collect()), - RefResult::Ref(Ref { id, .. }) => { + RefResult::Ref(Ref { id, .. }) | RefResult::RefDeprecated(id) => { let mut map = HashMap::new(); map.insert(String::new(), OutputRef(id)); Ok(map) diff --git a/buildkit-frontend/src/lib.rs b/buildkit-frontend/src/lib.rs index fe99419..be09e3f 100644 --- a/buildkit-frontend/src/lib.rs +++ b/buildkit-frontend/src/lib.rs @@ -151,9 +151,10 @@ where } } - // TODO: gracefully shutdown the HTTP/2 connection - - Ok(()) + // The HTTP/2 connection over stdio keeps tonic background tasks alive, + // preventing the tokio runtime from shutting down. Force-exit now that + // the result has been sent back to the daemon. + std::process::exit(0); } async fn frontend_entrypoint(bridge: &Bridge, frontend: F) -> Result From b0a2c6b0ecef51fe8dc63e7f304a4f3b5876ef3e Mon Sep 17 00:00:00 2001 From: Thomas DA ROCHA Date: Wed, 20 May 2026 17:41:05 +0200 Subject: [PATCH 15/21] test: Reverse example --- .ci/test-reverse-example.sh | 5 ++--- buildkit-frontend/examples/reverse.input | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.ci/test-reverse-example.sh b/.ci/test-reverse-example.sh index a6da57f..ef86cdf 100755 --- a/.ci/test-reverse-example.sh +++ b/.ci/test-reverse-example.sh @@ -1,13 +1,12 @@ #!/bin/bash source $(dirname $0)/common.sh -FRONTEND_LABEL="localhost:5000/rust-buildkit:reverse-frontend" -OUTPUT_LABEL="localhost:5000/rust-buildkit:reverse-image" +FRONTEND_LABEL="rust-buildkit:reverse-frontend" +OUTPUT_LABEL="rust-buildkit:reverse-image" set -ex docker build -t $FRONTEND_LABEL -f $EXAMPLES_DIR/reverse.dockerfile $WORKSPACE_DIR -docker push $FRONTEND_LABEL docker build -t $OUTPUT_LABEL -f $EXAMPLES_DIR/reverse.input $WORKSPACE_DIR diff --strip-trailing-cr --color=always <(cat $EXAMPLES_DIR/reverse.output) <(docker run --rm $OUTPUT_LABEL) diff --git a/buildkit-frontend/examples/reverse.input b/buildkit-frontend/examples/reverse.input index 8ffa1cc..adb059f 100644 --- a/buildkit-frontend/examples/reverse.input +++ b/buildkit-frontend/examples/reverse.input @@ -1,4 +1,4 @@ -# syntax = localhost:5000/rust-buildkit:reverse-frontend +# syntax = rust-buildkit:reverse-frontend Hello from the reverse example "dockerfile". Every line from this file has to be printed in reverse when you run the resulting image. From f3bd3f0a1b02bfa2c554f7ce7a94d2e86b248ad2 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 3 Jun 2026 12:46:39 +0000 Subject: [PATCH 16/21] Update buildkit-proto to BuildKit v0.30.0 Bumps the BUILDKIT_VERSION pin in update.sh from v0.29.0 to v0.30.0 and re-runs the script. The only schema delta between the two upstream tags on the five .proto files we vendor is a single new optional field on api/types/worker.proto: message BuildkitVersion { string package = 1; string version = 2; string revision = 3; string dockerfileVersion = 4; // added in v0.30.0 } gateway.proto, ops.proto, caps.proto and sourcepolicy.proto are byte-identical to v0.29.0 in this release, so no Rust source needs to change: the regenerated `BuildkitVersion` struct just gains a `dockerfile_version: String` field that callers can read or ignore. `cargo build --workspace --all-targets` and `cargo test --workspace` (44 tests: 21 in buildkit-frontend, 23 in buildkit-llb) both pass. https://claude.ai/code/session_01XtZHcL6rKJDuX7tUS3okdc --- .../proto/github.com/moby/buildkit/api/types/worker.proto | 3 ++- buildkit-proto/update.sh | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/buildkit-proto/proto/github.com/moby/buildkit/api/types/worker.proto b/buildkit-proto/proto/github.com/moby/buildkit/api/types/worker.proto index 8f56566..ef9382a 100644 --- a/buildkit-proto/proto/github.com/moby/buildkit/api/types/worker.proto +++ b/buildkit-proto/proto/github.com/moby/buildkit/api/types/worker.proto @@ -30,6 +30,7 @@ message BuildkitVersion { string package = 1; string version = 2; string revision = 3; + string dockerfileVersion = 4; } message CDIDevice { @@ -37,4 +38,4 @@ message CDIDevice { bool AutoAllow = 2; map Annotations = 3; bool OnDemand = 4; -} \ No newline at end of file +} diff --git a/buildkit-proto/update.sh b/buildkit-proto/update.sh index 6627ca8..fc606f9 100755 --- a/buildkit-proto/update.sh +++ b/buildkit-proto/update.sh @@ -1,7 +1,7 @@ #!/bin/sh set -e -export BUILDKIT_VERSION="v0.29.0" +export BUILDKIT_VERSION="v0.30.0" # Create all required directories mkdir -p proto/github.com/moby/buildkit/api/types From 526864403f9a6262cbb891cfcc36aac031322396 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 3 Jun 2026 12:47:29 +0000 Subject: [PATCH 17/21] buildkit-proto: implement Display for Platform instead of ToString `impl ToString for Platform` was added in 6cbf078 ("feat: Platform serialization"). It works, but clippy's `to_string_trait_impl` lint (now part of `clippy::all`) flags it because the standard library already ships a blanket `impl ToString for T`. Switch to `fmt::Display` so we get `to_string()` for free and `cargo clippy --workspace --all-targets -- -D warnings` is clean. No behavior change: the formatted strings are byte-for-byte the same, so existing `platform.to_string()` callers (notably `buildkit_llb::ops::platform::platform_id`) keep working. https://claude.ai/code/session_01XtZHcL6rKJDuX7tUS3okdc --- buildkit-proto/src/serialization/platform.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/buildkit-proto/src/serialization/platform.rs b/buildkit-proto/src/serialization/platform.rs index 1c6e1d6..ed64a03 100644 --- a/buildkit-proto/src/serialization/platform.rs +++ b/buildkit-proto/src/serialization/platform.rs @@ -1,3 +1,4 @@ +use std::fmt; use std::str::FromStr; use crate::pb::Platform; @@ -16,12 +17,12 @@ impl FromStr for Platform { } } -impl ToString for Platform { - fn to_string(&self) -> String { +impl fmt::Display for Platform { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if self.variant.is_empty() { - format!("{}/{}", self.os, self.architecture) + write!(f, "{}/{}", self.os, self.architecture) } else { - format!("{}/{}/{}", self.os, self.architecture, self.variant) + write!(f, "{}/{}/{}", self.os, self.architecture, self.variant) } } } From 126bebc8d39c2232d27b3496a12f7ac22da2ee44 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 3 Jun 2026 15:12:21 +0000 Subject: [PATCH 18/21] buildkit-llb, buildkit-frontend: bump Rust edition 2018 -> 2021 buildkit-proto has been on edition 2021 for a while (since the proto regeneration commits), but the two downstream crates were still pinned to 2018, which made the workspace a bit awkward: the same patterns compiled with different rules from one crate to the next, and 2021-only conveniences (disjoint closure captures, IntoIterator for arrays by value, expanded prelude with `TryFrom`/`TryInto`/ `FromIterator`) silently degraded as soon as code crossed the edition boundary. This aligns both crates on edition 2021 to match buildkit-proto. The bump compiles clean - `cargo build --workspace --all-targets` passes, `cargo fix --workspace --edition-idioms` finds no auto-fixable idiom drift, and the existing 44 tests still pass. While here, drops two `use std::convert::TryFrom;` statements in buildkit-frontend/src/oci.rs that became redundant: the 2021 prelude imports `TryFrom` (and `TryInto`) automatically, so the `impl TryFrom for ExposedPort` and the `u64::try_from(...)` call in the `opt_duration_nanos` helper module no longer need an explicit import. `cargo clippy --workspace --all-targets -- -D warnings` stays clean. https://claude.ai/code/session_01XtZHcL6rKJDuX7tUS3okdc --- buildkit-frontend/Cargo.toml | 2 +- buildkit-frontend/src/oci.rs | 2 -- buildkit-llb/Cargo.toml | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/buildkit-frontend/Cargo.toml b/buildkit-frontend/Cargo.toml index efaf68d..0b175a4 100644 --- a/buildkit-frontend/Cargo.toml +++ b/buildkit-frontend/Cargo.toml @@ -2,7 +2,7 @@ name = "buildkit-frontend" version = "0.3.0" authors = ["Denys Zariaiev "] -edition = "2018" +edition = "2021" description = "Foundation for BuildKit frontends implemented in Rust" documentation = "https://docs.rs/buildkit-frontend" diff --git a/buildkit-frontend/src/oci.rs b/buildkit-frontend/src/oci.rs index 326b0c8..7acc2d5 100644 --- a/buildkit-frontend/src/oci.rs +++ b/buildkit-frontend/src/oci.rs @@ -1,5 +1,4 @@ use std::collections::BTreeMap; -use std::convert::TryFrom; use std::path::PathBuf; use std::time::Duration; @@ -13,7 +12,6 @@ use serde_json::Value; /// containerd all read/write durations as nanos. mod opt_duration_nanos { use serde::{Deserialize, Deserializer, Serializer}; - use std::convert::TryFrom; use std::time::Duration; pub fn serialize(d: &Option, s: S) -> Result diff --git a/buildkit-llb/Cargo.toml b/buildkit-llb/Cargo.toml index 67d8ee9..2fafc6d 100644 --- a/buildkit-llb/Cargo.toml +++ b/buildkit-llb/Cargo.toml @@ -2,7 +2,7 @@ name = "buildkit-llb" version = "0.2.0" authors = ["Denys Zariaiev "] -edition = "2018" +edition = "2021" description = "Idiomatic high-level API to create BuildKit LLB graphs" documentation = "https://docs.rs/buildkit-llb" From d230692a4ab1b451f10b9aa727e7619351a60b91 Mon Sep 17 00:00:00 2001 From: Thomas DA ROCHA Date: Wed, 3 Jun 2026 17:26:37 +0200 Subject: [PATCH 19/21] feat: Implement missing commands and options --- buildkit-llb/src/lib.rs | 5 + buildkit-llb/src/ops/exec/command.rs | 171 ++++++++++++++++++++++++--- buildkit-llb/src/ops/exec/mount.rs | 49 ++++++++ buildkit-llb/src/ops/fs/copy.rs | 87 +++++++++++++- buildkit-llb/src/ops/fs/mkdir.rs | 24 +++- buildkit-llb/src/ops/fs/mkfile.rs | 24 +++- buildkit-llb/src/ops/fs/sequence.rs | 3 +- buildkit-llb/src/ops/merge.rs | 106 +++++++++++++++++ buildkit-llb/src/ops/mod.rs | 2 + buildkit-llb/src/ops/source/git.rs | 34 +++++- buildkit-llb/src/ops/source/http.rs | 15 +++ buildkit-llb/src/ops/source/image.rs | 1 - buildkit-llb/src/ops/terminal.rs | 4 +- 13 files changed, 490 insertions(+), 35 deletions(-) create mode 100644 buildkit-llb/src/ops/merge.rs diff --git a/buildkit-llb/src/lib.rs b/buildkit-llb/src/lib.rs index 4d551f9..720b6a7 100644 --- a/buildkit-llb/src/lib.rs +++ b/buildkit-llb/src/lib.rs @@ -22,3 +22,8 @@ pub mod prelude { pub use crate::ops::*; pub use crate::utils::{OperationOutput, OutputIdx, OwnOutputIdx}; } + +/// Re-export of the BuildKit protobuf types so callers can construct the +/// option structs (`ChownOpt`, `CacheOpt`, `SecretOpt`, ...) accepted by the +/// operation builders without depending on `buildkit-proto` directly. +pub use buildkit_proto::pb; diff --git a/buildkit-llb/src/ops/exec/command.rs b/buildkit-llb/src/ops/exec/command.rs index ea99616..fbdb491 100644 --- a/buildkit-llb/src/ops/exec/command.rs +++ b/buildkit-llb/src/ops/exec/command.rs @@ -29,6 +29,9 @@ pub struct Command<'a> { caps: HashMap, ignore_cache: bool, platform: Option, + network: NetMode, + security: SecurityMode, + secret_env: Vec, } impl<'a> Command<'a> { @@ -47,9 +50,38 @@ impl<'a> Command<'a> { caps: Default::default(), ignore_cache: false, platform: None, + network: NetMode::Unset, + security: SecurityMode::Sandbox, + secret_env: Vec::new(), } } + /// Expose a secret to the command as an environment variable + /// (`RUN --mount=type=secret,env=...`). + pub fn secret_env(mut self, secret: pb::SecretEnv) -> Self { + self.caps.insert("exec.mount.secret".into(), true); + self.secret_env.push(secret); + self + } + + /// Set the networking mode for the command (`RUN --network`). + pub fn network(mut self, network: NetMode) -> Self { + if let NetMode::Host = network { + self.caps.insert("exec.meta.network.host".into(), true); + } + self.network = network; + self + } + + /// Set the security mode for the command (`RUN --security`). + pub fn security(mut self, security: SecurityMode) -> Self { + if let SecurityMode::Insecure = security { + self.caps.insert("exec.meta.security.insecure".into(), true); + } + self.security = security; + self + } + /// Pin this exec op to a specific platform. The op will only be scheduled /// on a worker that advertises matching platform capabilities, which is /// what enables cross-compilation in multi-platform builds. @@ -113,21 +145,32 @@ impl<'a> Command<'a> { P: AsRef, { match mount { - Mount::Layer(..) | Mount::ReadOnlyLayer(..) | Mount::Scratch(..) => { + Mount::Layer(..) + | Mount::ReadOnlyLayer(..) + | Mount::ReadWriteLayer(..) + | Mount::Scratch(..) => { self.caps.insert("exec.mount.bind".into(), true); } - Mount::ReadOnlySelector(..) => { + Mount::ReadOnlySelector(..) | Mount::ReadWriteSelector(..) => { self.caps.insert("exec.mount.bind".into(), true); self.caps.insert("exec.mount.selector".into(), true); } - Mount::SharedCache(..) => { + Mount::SharedCache(..) | Mount::Cache(..) | Mount::CacheFrom(..) => { self.caps.insert("exec.mount.cache".into(), true); self.caps.insert("exec.mount.cache.sharing".into(), true); } - Mount::OptionalSshAgent(..) => { + Mount::Tmpfs(..) => { + self.caps.insert("exec.mount.tmpfs".into(), true); + } + + Mount::Secret(..) => { + self.caps.insert("exec.mount.secret".into(), true); + } + + Mount::OptionalSshAgent(..) | Mount::Ssh(..) => { self.caps.insert("exec.mount.ssh".into(), true); } } @@ -218,6 +261,40 @@ impl<'a> Operation for Command<'a> { ..Default::default() }, + Mount::ReadWriteLayer(_, destination) => pb::Mount { + input: last_input_index, + dest: destination.to_string_lossy().into(), + output: -1, + readonly: false, + mount_type: MountType::Bind as i32, + + ..Default::default() + }, + + Mount::ReadWriteSelector(_, destination, source) => pb::Mount { + input: last_input_index, + dest: destination.to_string_lossy().into(), + output: -1, + readonly: false, + selector: source.to_string_lossy().into(), + mount_type: MountType::Bind as i32, + + ..Default::default() + }, + + Mount::CacheFrom(_, destination, selector, opt, readonly) => pb::Mount { + input: last_input_index, + dest: destination.to_string_lossy().into(), + output: -1, + readonly: *readonly, + selector: selector.to_string_lossy().into(), + mount_type: MountType::Cache as i32, + + cache_opt: Some(opt.clone()), + + ..Default::default() + }, + Mount::Scratch(output, path) => { let mount = pb::Mount { input: -1, @@ -271,22 +348,84 @@ impl<'a> Operation for Command<'a> { return (Either::Right(empty()), mount); } + + Mount::Cache(path, opt, readonly) => { + let mount = pb::Mount { + input: -1, + dest: path.to_string_lossy().into(), + output: -1, + readonly: *readonly, + mount_type: MountType::Cache as i32, + + cache_opt: Some(opt.clone()), + + ..Default::default() + }; + + return (Either::Right(empty()), mount); + } + + Mount::Tmpfs(path, opt) => { + let mount = pb::Mount { + input: -1, + dest: path.to_string_lossy().into(), + output: -1, + mount_type: MountType::Tmpfs as i32, + + tmpfs_opt: Some(opt.clone()), + + ..Default::default() + }; + + return (Either::Right(empty()), mount); + } + + Mount::Secret(path, opt) => { + let mount = pb::Mount { + input: -1, + dest: path.to_string_lossy().into(), + output: -1, + mount_type: MountType::Secret as i32, + + secret_opt: Some(opt.clone()), + + ..Default::default() + }; + + return (Either::Right(empty()), mount); + } + + Mount::Ssh(path, opt) => { + let mount = pb::Mount { + input: -1, + dest: path.to_string_lossy().into(), + output: -1, + mount_type: MountType::Ssh as i32, + + ssh_opt: Some(opt.clone()), + + ..Default::default() + }; + + return (Either::Right(empty()), mount); + } }; let input = match mount { Mount::ReadOnlyLayer(input, ..) => input, Mount::ReadOnlySelector(input, ..) => input, + Mount::ReadWriteLayer(input, ..) => input, + Mount::ReadWriteSelector(input, ..) => input, + Mount::CacheFrom(input, ..) => input, Mount::Layer(_, input, ..) => input, - Mount::SharedCache(..) => { - unreachable!(); - } - - Mount::Scratch(..) => { - unreachable!(); - } - - Mount::OptionalSshAgent(..) => { + Mount::SharedCache(..) + | Mount::Cache(..) + | Mount::Tmpfs(..) + | Mount::Secret(..) + | Mount::Scratch(..) + | Mount::OptionalSshAgent(..) + | Mount::Ssh(..) => { unreachable!(); } }; @@ -307,10 +446,10 @@ impl<'a> Operation for Command<'a> { let head = pb::Op { op: Some(Op::Exec(ExecOp { mounts, - network: NetMode::Unset.into(), - security: SecurityMode::Sandbox.into(), + network: self.network.into(), + security: self.security.into(), meta: Some(self.context.clone().into()), - secretenv: Vec::new(), + secretenv: self.secret_env.clone(), cdi_devices: Vec::new(), })), diff --git a/buildkit-llb/src/ops/exec/mount.rs b/buildkit-llb/src/ops/exec/mount.rs index 76b541c..49578dc 100644 --- a/buildkit-llb/src/ops/exec/mount.rs +++ b/buildkit-llb/src/ops/exec/mount.rs @@ -1,5 +1,7 @@ use std::path::{Path, PathBuf}; +use buildkit_proto::pb; + use crate::utils::{OperationOutput, OutputIdx}; /// Operand of *command execution operation* that specifies how are input sources mounted. @@ -11,6 +13,13 @@ pub enum Mount<'a, P: AsRef> { /// Read-only output of another operation with a selector. ReadOnlySelector(OperationOutput<'a>, P, P), + /// Writable bind mount of another operation's output. Changes are not + /// persisted into the resulting layer (`--mount=type=bind,rw`). + ReadWriteLayer(OperationOutput<'a>, P), + + /// Writable bind mount of another operation's output with a selector. + ReadWriteSelector(OperationOutput<'a>, P, P), + /// Empty layer that produces an output. Scratch(OutputIdx, P), @@ -20,8 +29,26 @@ pub enum Mount<'a, P: AsRef> { /// Writable persistent cache. SharedCache(P), + /// Writable persistent cache with explicit options. + /// The boolean is the read-only flag. + Cache(P, pb::CacheOpt, bool), + + /// Persistent cache seeded from another operation's output. + /// Arguments: input layer, destination, source selector (empty for root), + /// cache options and the read-only flag. + CacheFrom(OperationOutput<'a>, P, P, pb::CacheOpt, bool), + + /// tmpfs mount with options (e.g. a size limit). + Tmpfs(P, pb::TmpfsOpt), + + /// Secret mount. + Secret(P, pb::SecretOpt), + /// Optional SSH agent socket at the specified path. OptionalSshAgent(P), + + /// SSH agent socket mount with explicit options. + Ssh(P, pb::SshOpt), } impl<'a, P: AsRef> Mount<'a, P> { @@ -35,10 +62,25 @@ impl<'a, P: AsRef> Mount<'a, P> { } ReadOnlyLayer(op, path) => ReadOnlyLayer(op, path.as_ref().into()), + ReadWriteSelector(op, path, selector) => { + ReadWriteSelector(op, path.as_ref().into(), selector.as_ref().into()) + } + ReadWriteLayer(op, path) => ReadWriteLayer(op, path.as_ref().into()), Scratch(output, path) => Scratch(output, path.as_ref().into()), Layer(output, input, path) => Layer(output, input, path.as_ref().into()), SharedCache(path) => SharedCache(path.as_ref().into()), + Cache(path, opt, readonly) => Cache(path.as_ref().into(), opt, readonly), + CacheFrom(op, path, selector, opt, readonly) => CacheFrom( + op, + path.as_ref().into(), + selector.as_ref().into(), + opt, + readonly, + ), + Tmpfs(path, opt) => Tmpfs(path.as_ref().into(), opt), + Secret(path, opt) => Secret(path.as_ref().into(), opt), OptionalSshAgent(path) => OptionalSshAgent(path.as_ref().into()), + Ssh(path, opt) => Ssh(path.as_ref().into(), opt), } } @@ -48,10 +90,17 @@ impl<'a, P: AsRef> Mount<'a, P> { let path = match self { ReadOnlySelector(_, path, ..) => path, ReadOnlyLayer(_, path) => path, + ReadWriteSelector(_, path, ..) => path, + ReadWriteLayer(_, path) => path, Scratch(_, path) => path, Layer(_, _, path) => path, SharedCache(path) => path, + Cache(path, ..) => path, + CacheFrom(_, path, ..) => path, + Tmpfs(path, ..) => path, + Secret(_, _) => return false, OptionalSshAgent(_) => return false, + Ssh(_, _) => return false, }; path.as_ref() == Path::new("/") diff --git a/buildkit-llb/src/ops/fs/copy.rs b/buildkit-llb/src/ops/fs/copy.rs index a4b2cae..d4af0c7 100644 --- a/buildkit-llb/src/ops/fs/copy.rs +++ b/buildkit-llb/src/ops/fs/copy.rs @@ -20,6 +20,13 @@ pub struct CopyOperation { create_path: bool, wildcard: bool, + mode: i32, + mode_str: String, + owner: Option, + include_patterns: Vec, + exclude_patterns: Vec, + unpack: bool, + description: HashMap, caps: HashMap, } @@ -43,6 +50,13 @@ impl OpWithoutSource { create_path: false, wildcard: false, + mode: -1, + mode_str: String::new(), + owner: None, + include_patterns: Vec::new(), + exclude_patterns: Vec::new(), + unpack: false, + caps, description: Default::default(), } @@ -61,6 +75,13 @@ impl OpWithoutSource { create_path: self.create_path, wildcard: self.wildcard, + mode: self.mode, + mode_str: self.mode_str, + owner: self.owner, + include_patterns: self.include_patterns, + exclude_patterns: self.exclude_patterns, + unpack: self.unpack, + description: self.description, caps: self.caps, } @@ -81,6 +102,13 @@ impl<'a> OpWithSource<'a> { create_path: self.create_path, wildcard: self.wildcard, + mode: self.mode, + mode_str: self.mode_str, + owner: self.owner, + include_patterns: self.include_patterns, + exclude_patterns: self.exclude_patterns, + unpack: self.unpack, + description: self.description, caps: self.caps, } @@ -117,6 +145,56 @@ where self.wildcard = value; self } + + /// Override the permission bits of the copied files (`COPY --chmod`). + /// Pass the mode as an integer (e.g. `0o755`); `-1` keeps the source mode. + pub fn chmod(mut self, mode: i32) -> Self { + self.mode = mode; + self + } + + /// Override the permissions of the copied files using a non-octal mode + /// string (used when the value can't be represented as octal bits). + pub fn chmod_str(mut self, mode: S) -> Self + where + S: Into, + { + self.mode_str = mode.into(); + self + } + + /// Override the owner of the copied files (`COPY --chown`). + pub fn chown(mut self, owner: pb::ChownOpt) -> Self { + self.owner = Some(owner); + self + } + + /// Only copy files/directories matching at least one of these patterns. + pub fn include_patterns(mut self, patterns: I) -> Self + where + I: IntoIterator, + S: Into, + { + self.include_patterns = patterns.into_iter().map(Into::into).collect(); + self + } + + /// Exclude files/directories matching any of these patterns (`COPY --exclude`). + pub fn exclude_patterns(mut self, patterns: I) -> Self + where + I: IntoIterator, + S: Into, + { + self.exclude_patterns = patterns.into_iter().map(Into::into).collect(); + self + } + + /// Automatically unpack a source archive into the destination (`ADD` archive + /// behaviour). + pub fn unpack(mut self, value: bool) -> Self { + self.unpack = value; + self + } } impl<'a> FileOperation for OpWithDestination<'a> { @@ -201,8 +279,13 @@ impl<'a> FileOperation for OpWithDestination<'a> { create_dest_path: self.create_path, allow_wildcard: self.wildcard, - // TODO: make this configurable - mode: -1, + owner: self.owner.clone(), + mode: self.mode, + mode_str: self.mode_str.clone(), + + attempt_unpack_docker_compatibility: self.unpack, + include_patterns: self.include_patterns.clone(), + exclude_patterns: self.exclude_patterns.clone(), // TODO: make this configurable timestamp: -1, diff --git a/buildkit-llb/src/ops/fs/mkdir.rs b/buildkit-llb/src/ops/fs/mkdir.rs index e679c0e..bf60ac5 100644 --- a/buildkit-llb/src/ops/fs/mkdir.rs +++ b/buildkit-llb/src/ops/fs/mkdir.rs @@ -15,6 +15,8 @@ pub struct MakeDirOperation<'a> { output: OutputIdx, make_parents: bool, + mode: i32, + owner: Option, // description: HashMap, // caps: HashMap, } @@ -32,6 +34,8 @@ impl<'a> MakeDirOperation<'a> { output, make_parents: false, + mode: -1, + owner: None, // caps, // description: Default::default(), } @@ -42,6 +46,19 @@ impl<'a> MakeDirOperation<'a> { self } + /// Override the permission bits of the created directory. Pass the mode as + /// an integer (e.g. `0o755`); `-1` uses the BuildKit default. + pub fn chmod(mut self, mode: i32) -> Self { + self.mode = mode; + self + } + + /// Override the owner of the created directory. + pub fn chown(mut self, owner: pb::ChownOpt) -> Self { + self.owner = Some(owner); + self + } + pub fn into_operation(self) -> super::sequence::SequenceOperation<'a> { super::sequence::SequenceOperation::new().append(self) } @@ -94,14 +111,11 @@ impl<'a> FileOperation for MakeDirOperation<'a> { make_parents: self.make_parents, - // TODO: make this configurable - mode: -1, + mode: self.mode, + owner: self.owner.clone(), // TODO: make this configurable timestamp: -1, - - // TODO: make this configurable - owner: None, })), }) } diff --git a/buildkit-llb/src/ops/fs/mkfile.rs b/buildkit-llb/src/ops/fs/mkfile.rs index 382e6ea..a24a5aa 100644 --- a/buildkit-llb/src/ops/fs/mkfile.rs +++ b/buildkit-llb/src/ops/fs/mkfile.rs @@ -15,6 +15,8 @@ pub struct MakeFileOperation<'a> { output: OutputIdx, data: Option>, + mode: i32, + owner: Option, // description: HashMap, // caps: HashMap, } @@ -32,6 +34,8 @@ impl<'a> MakeFileOperation<'a> { output, data: None, + mode: -1, + owner: None, // caps, // description: Default::default(), } @@ -42,6 +46,19 @@ impl<'a> MakeFileOperation<'a> { self } + /// Override the permission bits of the created file. Pass the mode as an + /// integer (e.g. `0o644`); `-1` uses the BuildKit default. + pub fn chmod(mut self, mode: i32) -> Self { + self.mode = mode; + self + } + + /// Override the owner of the created file. + pub fn chown(mut self, owner: pb::ChownOpt) -> Self { + self.owner = Some(owner); + self + } + pub fn into_operation(self) -> super::sequence::SequenceOperation<'a> { super::sequence::SequenceOperation::new().append(self) } @@ -94,14 +111,11 @@ impl<'a> FileOperation for MakeFileOperation<'a> { data: self.data.clone().unwrap_or_else(|| Vec::with_capacity(0)), - // TODO: make this configurable - mode: -1, + mode: self.mode, + owner: self.owner.clone(), // TODO: make this configurable timestamp: -1, - - // TODO: make this configurable - owner: None, })), }) } diff --git a/buildkit-llb/src/ops/fs/sequence.rs b/buildkit-llb/src/ops/fs/sequence.rs index e6908b9..e2bcb56 100644 --- a/buildkit-llb/src/ops/fs/sequence.rs +++ b/buildkit-llb/src/ops/fs/sequence.rs @@ -48,7 +48,8 @@ impl<'a> SequenceOperation<'a> { // TODO: make sure the `inner` elements have monotonic indexes self.inner - .iter().rfind(|fs| fs.output() >= 0) + .iter() + .rfind(|fs| fs.output() >= 0) .map(|fs| fs.output() as u32) } } diff --git a/buildkit-llb/src/ops/merge.rs b/buildkit-llb/src/ops/merge.rs new file mode 100644 index 0000000..666fbf7 --- /dev/null +++ b/buildkit-llb/src/ops/merge.rs @@ -0,0 +1,106 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use buildkit_proto::pb::{self, op::Op, MergeInput, MergeOp, OpMetadata}; + +use crate::ops::{OperationBuilder, SingleBorrowedOutput, SingleOwnedOutput}; +use crate::serialization::{Context, Node, Operation, OperationId, Result}; +use crate::utils::{OperationOutput, OutputIdx}; + +/// Merges the layers of several operations into a single output. This is what a +/// Dockerfile's `COPY --link` is translated to: the copy is performed on an +/// independent layer which is then merged on top of the previous state, so the +/// resulting layer stays cacheable independently of the base. +#[derive(Debug)] +pub struct MergeOperation<'a> { + id: OperationId, + inputs: Vec>, + description: HashMap, + ignore_cache: bool, +} + +impl<'a> MergeOperation<'a> { + /// Create a merge of the given operation outputs, in order (later inputs + /// are layered on top of earlier ones). + pub fn new(inputs: Vec>) -> Self { + Self { + id: OperationId::default(), + inputs, + description: Default::default(), + ignore_cache: false, + } + } +} + +impl<'a> SingleBorrowedOutput<'a> for MergeOperation<'a> { + fn output(&'a self) -> OperationOutput<'a> { + OperationOutput::borrowed(self, OutputIdx(0)) + } +} + +impl<'a> SingleOwnedOutput<'a> for Arc> { + fn output(&self) -> OperationOutput<'a> { + OperationOutput::owned(self.clone(), OutputIdx(0)) + } +} + +impl<'a> OperationBuilder<'a> for MergeOperation<'a> { + fn custom_name(mut self, name: S) -> Self + where + S: Into, + { + self.description + .insert("llb.customname".into(), name.into()); + + self + } + + fn ignore_cache(mut self, ignore: bool) -> Self { + self.ignore_cache = ignore; + self + } +} + +impl<'a> Operation for MergeOperation<'a> { + fn id(&self) -> &OperationId { + &self.id + } + + fn serialize(&self, cx: &mut Context) -> Result { + let mut inputs = Vec::with_capacity(self.inputs.len()); + let mut merge_inputs = Vec::with_capacity(self.inputs.len()); + + for (index, input) in self.inputs.iter().enumerate() { + let serialized = cx.register(input.operation())?; + inputs.push(pb::Input { + digest: serialized.digest.clone(), + index: input.output().into(), + }); + merge_inputs.push(MergeInput { + input: index as i64, + }); + } + + let head = pb::Op { + inputs, + op: Some(Op::Merge(MergeOp { + inputs: merge_inputs, + })), + + ..Default::default() + }; + + let mut caps = HashMap::new(); + caps.insert("mergeop".into(), true); + + let metadata = OpMetadata { + description: self.description.clone(), + ignore_cache: self.ignore_cache, + caps, + + ..Default::default() + }; + + Ok(Node::new(head, metadata)) + } +} diff --git a/buildkit-llb/src/ops/mod.rs b/buildkit-llb/src/ops/mod.rs index 4d85883..f5ad549 100644 --- a/buildkit-llb/src/ops/mod.rs +++ b/buildkit-llb/src/ops/mod.rs @@ -2,12 +2,14 @@ use std::sync::Arc; pub mod exec; pub mod fs; +pub mod merge; pub mod platform; pub mod source; pub mod terminal; pub use self::exec::Command; pub use self::fs::FileSystem; +pub use self::merge::MergeOperation; pub use self::platform::Platform; pub use self::source::Source; pub use self::terminal::Terminal; diff --git a/buildkit-llb/src/ops/source/git.rs b/buildkit-llb/src/ops/source/git.rs index e3df6d2..1126ed0 100644 --- a/buildkit-llb/src/ops/source/git.rs +++ b/buildkit-llb/src/ops/source/git.rs @@ -12,6 +12,8 @@ pub struct GitSource { id: OperationId, remote: String, reference: Option, + keep_git_dir: bool, + checksum: Option, description: HashMap, ignore_cache: bool, } @@ -38,6 +40,8 @@ impl GitSource { id: OperationId::default(), remote, reference: None, + keep_git_dir: false, + checksum: None, description: Default::default(), ignore_cache: false, } @@ -52,6 +56,23 @@ impl GitSource { self.reference = Some(reference.into()); self } + + /// Keep the `.git` directory in the checked-out source + /// (`ADD --keep-git-dir`). + pub fn with_keep_git_dir(mut self, keep: bool) -> Self { + self.keep_git_dir = keep; + self + } + + /// Validate the resolved commit against the given checksum (`ADD --checksum`). + /// For Git sources the checksum is the commit SHA (full or a prefix). + pub fn with_checksum(mut self, checksum: S) -> Self + where + S: Into, + { + self.checksum = Some(checksum.into()); + self + } } impl<'a> SingleBorrowedOutput<'a> for GitSource { @@ -95,11 +116,16 @@ impl Operation for GitSource { format!("git://{}", self.remote) }; + let mut attrs = HashMap::default(); + if self.keep_git_dir { + attrs.insert("git.keepgitdir".into(), "true".into()); + } + if let Some(ref checksum) = self.checksum { + attrs.insert("git.checksum".into(), checksum.clone()); + } + let head = pb::Op { - op: Some(Op::Source(SourceOp { - identifier, - attrs: Default::default(), - })), + op: Some(Op::Source(SourceOp { identifier, attrs })), ..Default::default() }; diff --git a/buildkit-llb/src/ops/source/http.rs b/buildkit-llb/src/ops/source/http.rs index 98c4350..face445 100644 --- a/buildkit-llb/src/ops/source/http.rs +++ b/buildkit-llb/src/ops/source/http.rs @@ -12,6 +12,7 @@ pub struct HttpSource { id: OperationId, url: String, file_name: Option, + checksum: Option, description: HashMap, ignore_cache: bool, } @@ -25,6 +26,7 @@ impl HttpSource { id: OperationId::default(), url: url.into(), file_name: None, + checksum: None, description: Default::default(), ignore_cache: false, } @@ -39,6 +41,15 @@ impl HttpSource { self.file_name = Some(name.into()); self } + + /// Validate the downloaded file against the given digest (`ADD --checksum`). + pub fn with_checksum(mut self, checksum: S) -> Self + where + S: Into, + { + self.checksum = Some(checksum.into()); + self + } } impl<'a> SingleBorrowedOutput<'a> for HttpSource { @@ -82,6 +93,10 @@ impl Operation for HttpSource { attrs.insert("http.filename".into(), file_name.into()); } + if let Some(ref checksum) = self.checksum { + attrs.insert("http.checksum".into(), checksum.into()); + } + let head = pb::Op { op: Some(Op::Source(SourceOp { identifier: self.url.clone(), diff --git a/buildkit-llb/src/ops/source/image.rs b/buildkit-llb/src/ops/source/image.rs index 6acb0e2..1d08cf2 100644 --- a/buildkit-llb/src/ops/source/image.rs +++ b/buildkit-llb/src/ops/source/image.rs @@ -44,7 +44,6 @@ impl fmt::Display for ResolveMode { } } - lazy_static! { static ref TAG_EXPR: Regex = Regex::new(r":[\w][\w.-]+$").unwrap(); } diff --git a/buildkit-llb/src/ops/terminal.rs b/buildkit-llb/src/ops/terminal.rs index 7f236dc..8234a04 100644 --- a/buildkit-llb/src/ops/terminal.rs +++ b/buildkit-llb/src/ops/terminal.rs @@ -123,7 +123,9 @@ fn serialization() { let mut metadata_digests = { definition - .metadata.keys().map(|digest| digest.as_str()) + .metadata + .keys() + .map(|digest| digest.as_str()) .collect::>() }; From 1ece428ec0b45c3ba0b6ae7f95f810de7fd9cd6b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 3 Jun 2026 15:37:51 +0000 Subject: [PATCH 20/21] buildkit-frontend: refresh stale dev-dependencies Aligns dev-dep pins with the rest of the workspace and brings them up to current. None of these affect the public API or runtime - they only gate the example and test compilation: - env_logger 0.6 -> 0.11 (0.6 dates from 2019) - pretty_assertions 0.6 -> 1.4 (post-1.0 stable) - regex 1.3 -> 1.11 (buildkit-llb already uses 1.11.x) - url 2.1 -> 2.5 `cargo update --dry-run --verbose` still reports five upstream majors behind (`tonic`/`tonic-build` 0.12 -> 0.14, `prost`/`prost-types` 0.13 -> 0.14, `sha2` 0.10 -> 0.11) - those are intentional API breaks comparable to the 0.1 -> 0.12 tonic migration we did earlier and warrant separate, scoped commits, not a drive-by bump. `cargo test --workspace` (44 tests) and `cargo clippy --workspace --all-targets -- -D warnings` both stay clean. https://claude.ai/code/session_01XtZHcL6rKJDuX7tUS3okdc --- buildkit-frontend/Cargo.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/buildkit-frontend/Cargo.toml b/buildkit-frontend/Cargo.toml index 0b175a4..8e962dc 100644 --- a/buildkit-frontend/Cargo.toml +++ b/buildkit-frontend/Cargo.toml @@ -45,10 +45,10 @@ path = "../buildkit-llb" [dev-dependencies] async-trait = "0.1" -env_logger = "0.6" -pretty_assertions = "0.6" -regex = "1.3" -url = "2.1" +env_logger = "0.11" +pretty_assertions = "1.4" +regex = "1.11" +url = "2.5" [dev-dependencies.tokio] version = "1" From e36ebe935764c9f28e04b8c820a0eded571dcb94 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 3 Jun 2026 15:39:32 +0000 Subject: [PATCH 21/21] buildkit-llb: fix clone_on_copy lint on Tmpfs mount `pb::TmpfsOpt` is `#[derive(Copy)]` since prost only ever generates `Copy` for plain-scalar messages, so the `opt.clone()` introduced in d230692 ("feat: Implement missing commands and options") trips clippy::clone_on_copy. Just dereference the borrow. No behavior change. https://claude.ai/code/session_01XtZHcL6rKJDuX7tUS3okdc --- buildkit-llb/src/ops/exec/command.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildkit-llb/src/ops/exec/command.rs b/buildkit-llb/src/ops/exec/command.rs index fbdb491..13a45a8 100644 --- a/buildkit-llb/src/ops/exec/command.rs +++ b/buildkit-llb/src/ops/exec/command.rs @@ -372,7 +372,7 @@ impl<'a> Operation for Command<'a> { output: -1, mount_type: MountType::Tmpfs as i32, - tmpfs_opt: Some(opt.clone()), + tmpfs_opt: Some(*opt), ..Default::default() };