1+ // Copyright 2025 Google LLC
2+ //
3+ // Licensed under the Apache License, Version 2.0 (the "License");
4+ // you may not use this file except in compliance with the License.
5+ // You may obtain a copy of the License at
6+ //
7+ // http://www.apache.org/licenses/LICENSE-2.0
8+ //
9+ // Unless required by applicable law or agreed to in writing, software
10+ // distributed under the License is distributed on an "AS IS" BASIS,
11+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+ // See the License for the specific language governing permissions and
13+ // limitations under the License.
14+
15+ package generate
16+
17+ import (
18+ "archive/zip"
19+ "context"
20+ "errors"
21+ "fmt"
22+ "io"
23+ "log/slog"
24+ "os"
25+ "path/filepath"
26+ "strings"
27+
28+ "cloud.google.com/java/internal/librariangen/bazel"
29+ "cloud.google.com/java/internal/librariangen/execv"
30+ "cloud.google.com/java/internal/librariangen/protoc"
31+ "cloud.google.com/java/internal/librariangen/request"
32+ )
33+
34+ // Test substitution vars.
35+ var (
36+ bazelParse = bazel .Parse
37+ execvRun = execv .Run
38+ requestParse = request .ParseLibrary
39+ protocBuild = protoc .Build
40+ )
41+
42+ // Config holds the internal librariangen configuration for the generate command.
43+ type Config struct {
44+ // LibrarianDir is the path to the librarian-tool input directory.
45+ // It is expected to contain the generate-request.json file.
46+ LibrarianDir string
47+ // InputDir is the path to the .librarian/generator-input directory from the
48+ // language repository.
49+ InputDir string
50+ // OutputDir is the path to the empty directory where librariangen writes
51+ // its output.
52+ OutputDir string
53+ // SourceDir is the path to a complete checkout of the googleapis repository.
54+ SourceDir string
55+ }
56+
57+ // Validate ensures that the configuration is valid.
58+ func (c * Config ) Validate () error {
59+ if c .LibrarianDir == "" {
60+ return errors .New ("librariangen: librarian directory must be set" )
61+ }
62+ if c .InputDir == "" {
63+ return errors .New ("librariangen: input directory must be set" )
64+ }
65+ if c .OutputDir == "" {
66+ return errors .New ("librariangen: output directory must be set" )
67+ }
68+ if c .SourceDir == "" {
69+ return errors .New ("librariangen: source directory must be set" )
70+ }
71+ return nil
72+ }
73+
74+ // Generate is the main entrypoint for the `generate` command. It orchestrates
75+ // the entire generation process.
76+ func Generate (ctx context.Context , cfg * Config ) error {
77+ if err := cfg .Validate (); err != nil {
78+ return fmt .Errorf ("librariangen: invalid configuration: %w" , err )
79+ }
80+ slog .Debug ("librariangen: generate command started" )
81+ defer cleanupIntermediateFiles (cfg .OutputDir )
82+
83+ generateReq , err := readGenerateReq (cfg .LibrarianDir )
84+ if err != nil {
85+ return fmt .Errorf ("librariangen: failed to read request: %w" , err )
86+ }
87+
88+ if err := invokeProtoc (ctx , cfg , generateReq ); err != nil {
89+ return fmt .Errorf ("librariangen: gapic generation failed: %w" , err )
90+ }
91+
92+ // Unzip the generated zip file.
93+ zipPath := filepath .Join (cfg .OutputDir , "java_gapic.zip" )
94+ if err := unzip (zipPath , cfg .OutputDir ); err != nil {
95+ return fmt .Errorf ("librariangen: failed to unzip %s: %w" , zipPath , err )
96+ }
97+
98+ // Unzip the inner temp-codegen.srcjar.
99+ srcjarPath := filepath .Join (cfg .OutputDir , "temp-codegen.srcjar" )
100+ srcjarDest := filepath .Join (cfg .OutputDir , "java_gapic_srcjar" )
101+ if err := unzip (srcjarPath , srcjarDest ); err != nil {
102+ return fmt .Errorf ("librariangen: failed to unzip %s: %w" , srcjarPath , err )
103+ }
104+
105+ if err := restructureOutput (cfg .OutputDir , generateReq .ID ); err != nil {
106+ return fmt .Errorf ("librariangen: failed to restructure output: %w" , err )
107+ }
108+
109+ slog .Debug ("librariangen: generate command finished" )
110+ return nil
111+ }
112+
113+ // invokeProtoc handles the protoc GAPIC generation logic for the 'generate' CLI command.
114+ // It reads a request file, and for each API specified, it invokes protoc
115+ // to generate the client library. It returns the module path and the path to the service YAML.
116+ func invokeProtoc (ctx context.Context , cfg * Config , generateReq * request.Library ) error {
117+ for _ , api := range generateReq .APIs {
118+ apiServiceDir := filepath .Join (cfg .SourceDir , api .Path )
119+ slog .Info ("processing api" , "service_dir" , apiServiceDir )
120+ bazelConfig , err := bazelParse (apiServiceDir )
121+ if err != nil {
122+ return fmt .Errorf ("librariangen: failed to parse BUILD.bazel for %s: %w" , apiServiceDir , err )
123+ }
124+ args , err := protocBuild (apiServiceDir , bazelConfig , cfg .SourceDir , cfg .OutputDir )
125+ if err != nil {
126+ return fmt .Errorf ("librariangen: failed to build protoc command for api %q in library %q: %w" , api .Path , generateReq .ID , err )
127+ }
128+ if err := execvRun (ctx , args , cfg .OutputDir ); err != nil {
129+ return fmt .Errorf ("librariangen: protoc failed for api %q in library %q: %w" , api .Path , generateReq .ID , err )
130+ }
131+ }
132+ return nil
133+ }
134+
135+ // readGenerateReq reads generate-request.json from the librarian-tool input directory.
136+ // The request file tells librariangen which library and APIs to generate.
137+ // It is prepared by the Librarian tool and mounted at /librarian.
138+ func readGenerateReq (librarianDir string ) (* request.Library , error ) {
139+ reqPath := filepath .Join (librarianDir , "generate-request.json" )
140+ slog .Debug ("librariangen: reading generate request" , "path" , reqPath )
141+
142+ generateReq , err := requestParse (reqPath )
143+ if err != nil {
144+ return nil , err
145+ }
146+ slog .Debug ("librariangen: successfully unmarshalled request" , "library_id" , generateReq .ID )
147+ return generateReq , nil
148+ }
149+
150+ // moveFiles moves all files (and directories) from sourceDir to targetDir.
151+ func moveFiles (sourceDir , targetDir string ) error {
152+ files , err := os .ReadDir (sourceDir )
153+ if err != nil {
154+ return fmt .Errorf ("librariangen: failed to read dir %s: %w" , sourceDir , err )
155+ }
156+ for _ , f := range files {
157+ oldPath := filepath .Join (sourceDir , f .Name ())
158+ newPath := filepath .Join (targetDir , f .Name ())
159+ slog .Debug ("librariangen: moving file" , "from" , oldPath , "to" , newPath )
160+ if err := os .Rename (oldPath , newPath ); err != nil {
161+ return fmt .Errorf ("librariangen: failed to move %s to %s: %w" , oldPath , newPath , err )
162+ }
163+ }
164+ return nil
165+ }
166+
167+ func restructureOutput (outputDir , libraryID string ) error {
168+ slog .Debug ("librariangen: restructuring output directory" , "dir" , outputDir )
169+
170+ // Define source and destination directories.
171+ gapicSrcDir := filepath .Join (outputDir , "java_gapic_srcjar" , "src" , "main" , "java" )
172+ gapicTestDir := filepath .Join (outputDir , "java_gapic_srcjar" , "src" , "test" , "java" )
173+ protoSrcDir := filepath .Join (outputDir , "com" )
174+ samplesDir := filepath .Join (outputDir , "java_gapic_srcjar" , "samples" , "snippets" )
175+
176+ gapicDestDir := filepath .Join (outputDir , fmt .Sprintf ("google-cloud-%s" , libraryID ), "src" , "main" , "java" )
177+ gapicTestDestDir := filepath .Join (outputDir , fmt .Sprintf ("google-cloud-%s" , libraryID ), "src" , "test" , "java" )
178+ protoDestDir := filepath .Join (outputDir , fmt .Sprintf ("proto-google-cloud-%s-v1" , libraryID ), "src" , "main" , "java" )
179+ samplesDestDir := filepath .Join (outputDir , "samples" , "snippets" )
180+
181+ // Create destination directories.
182+ destDirs := []string {gapicDestDir , gapicTestDestDir , protoDestDir , samplesDestDir }
183+ for _ , dir := range destDirs {
184+ if err := os .MkdirAll (dir , 0755 ); err != nil {
185+ return err
186+ }
187+ }
188+
189+ // Move files.
190+ moves := map [string ]string {
191+ gapicSrcDir : gapicDestDir ,
192+ gapicTestDir : gapicTestDestDir ,
193+ protoSrcDir : protoDestDir ,
194+ samplesDir : samplesDestDir ,
195+ }
196+ for src , dest := range moves {
197+ if err := moveFiles (src , dest ); err != nil {
198+ return err
199+ }
200+ }
201+
202+ return nil
203+ }
204+
205+ func cleanupIntermediateFiles (outputDir string ) {
206+ slog .Debug ("librariangen: cleaning up intermediate files" , "dir" , outputDir )
207+ filesToRemove := []string {
208+ "java_gapic_srcjar" ,
209+ "com" ,
210+ "java_gapic.zip" ,
211+ "temp-codegen.srcjar" ,
212+ }
213+ for _ , file := range filesToRemove {
214+ path := filepath .Join (outputDir , file )
215+ if err := os .RemoveAll (path ); err != nil {
216+ slog .Error ("librariangen: failed to clean up intermediate file" , "path" , path , "error" , err )
217+ }
218+ }
219+ }
220+
221+ func unzip (src , dest string ) error {
222+ r , err := zip .OpenReader (src )
223+ if err != nil {
224+ return err
225+ }
226+ defer r .Close ()
227+
228+ for _ , f := range r .File {
229+ fpath := filepath .Join (dest , f .Name )
230+
231+ if ! strings .HasPrefix (fpath , filepath .Clean (dest )+ string (os .PathSeparator )) {
232+ return fmt .Errorf ("librariangen: illegal file path: %s" , fpath )
233+ }
234+
235+ if f .FileInfo ().IsDir () {
236+ os .MkdirAll (fpath , os .ModePerm )
237+ continue
238+ }
239+
240+ if err := os .MkdirAll (filepath .Dir (fpath ), os .ModePerm ); err != nil {
241+ return err
242+ }
243+
244+ outFile , err := os .OpenFile (fpath , os .O_WRONLY | os .O_CREATE | os .O_TRUNC , f .Mode ())
245+ if err != nil {
246+ return err
247+ }
248+
249+ rc , err := f .Open ()
250+ if err != nil {
251+ outFile .Close ()
252+ return err
253+ }
254+
255+ _ , copyErr := io .Copy (outFile , rc )
256+ rc .Close () // Error on read-only file close is less critical
257+ closeErr := outFile .Close ()
258+
259+ if copyErr != nil {
260+ return copyErr
261+ }
262+ if closeErr != nil {
263+ return closeErr
264+ }
265+ }
266+ return nil
267+ }
0 commit comments