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