Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f42f40c
feat(java): append versions.txt on add
noahdietz Jul 6, 2026
9bc37cb
Merge branch 'main' of https://github.com/googleapis/librarian into j…
noahdietz Jul 6, 2026
6af76b3
fix renaming
noahdietz Jul 6, 2026
19ba48b
remove unnecessary test code
noahdietz Jul 6, 2026
d8418e9
address feedback
noahdietz Jul 6, 2026
54138cf
Merge branch 'main' into java-add-version-txt
noahdietz Jul 6, 2026
0e49b8c
Merge branch 'main' into java-add-version-txt
noahdietz Jul 6, 2026
f5e3571
released_version only for new libs
noahdietz Jul 7, 2026
cf8cf63
Merge branch 'java-add-version-txt' of https://github.com/noahdietz/l…
noahdietz Jul 7, 2026
4ce959c
fix test
noahdietz Jul 7, 2026
272f487
Merge branch 'main' into java-add-version-txt
noahdietz Jul 7, 2026
136d5d7
Merge branch 'main' into java-add-version-txt
noahdietz Jul 7, 2026
474686c
Merge branch 'main' into java-add-version-txt
noahdietz Jul 7, 2026
dbcf64b
Merge branch 'main' into java-add-version-txt
noahdietz Jul 8, 2026
f8443ca
refactor to deduplicate code
noahdietz Jul 8, 2026
ab33ca1
Merge branch 'main' into java-add-version-txt
noahdietz Jul 8, 2026
038ad51
Merge branch 'main' into java-add-version-txt
noahdietz Jul 9, 2026
dd33419
Merge branch 'main' into java-add-version-txt
noahdietz Jul 10, 2026
3f2140d
address refactor feedback
noahdietz Jul 10, 2026
d859a13
refactor TestAdd
noahdietz Jul 10, 2026
6977eb0
Merge branch 'main' into java-add-version-txt
noahdietz Jul 10, 2026
f8e5a88
Merge branch 'main' into java-add-version-txt
noahdietz Jul 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions internal/librarian/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,11 @@ func addNewLibrary(cfg *config.Config, api *config.API) (string, *config.Config,
case config.LanguageGo:
lib = golang.Add(lib)
case config.LanguageJava:
lib = java.Add(lib)
var err error
lib, err = java.Add(lib, nil)
if err != nil {
return "", nil, err
}
case config.LanguagePython:
var err error
lib, err = python.Add(cfg, lib)
Expand Down Expand Up @@ -275,8 +279,15 @@ func updateExistingLibrary(cfg *config.Config, existingLib *config.Library, api
case config.LanguageGo:
existingLib.APIs = append(existingLib.APIs, api)
existingLib = golang.Add(existingLib)
case config.LanguageJava, config.LanguageNodejs:
case config.LanguageNodejs:
existingLib.APIs = append(existingLib.APIs, api)
case config.LanguageJava:
existingLib.APIs = append(existingLib.APIs, api)
var err error
existingLib, err = java.Add(existingLib, api)
if err != nil {
return "", nil, err
}
default:
return "", nil, fmt.Errorf("%w: %s", errLibraryAlreadyExists, existingLib.Name)
}
Expand Down
17 changes: 17 additions & 0 deletions internal/librarian/add_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package librarian

import (
"errors"
"os"
"path/filepath"
"sort"
"strconv"
Expand Down Expand Up @@ -165,6 +166,9 @@ func TestAddCommand(t *testing.T) {
t.Run(test.name, func(t *testing.T) {
tmpDir := t.TempDir()
t.Chdir(tmpDir)
if err := os.WriteFile(filepath.Join(tmpDir, "versions.txt"), nil, 0644); err != nil {
t.Fatal(err)
}

cfg := sample.Config()
cfg.Default.Output = "output"
Expand Down Expand Up @@ -220,6 +224,9 @@ func TestAddLibrary(t *testing.T) {
t.Run(test.name, func(t *testing.T) {
tmpDir := t.TempDir()
t.Chdir(tmpDir)
if err := os.WriteFile(filepath.Join(tmpDir, "versions.txt"), nil, 0644); err != nil {
t.Fatal(err)
}

cfg := sample.Config()
cfg.Libraries = []*config.Library{
Expand Down Expand Up @@ -395,6 +402,7 @@ func TestAddLibrary_ExistingLibrary(t *testing.T) {
{Path: "google/cloud/secretmanager/v1"},
{Path: "google/cloud/secretmanager/v1beta2"},
},
Java: &config.JavaModule{ReleasedVersion: "1.2.3"},
},
},
},
Expand All @@ -403,6 +411,9 @@ func TestAddLibrary_ExistingLibrary(t *testing.T) {
t.Run(test.name, func(t *testing.T) {
tmpDir := t.TempDir()
t.Chdir(tmpDir)
if err := os.WriteFile(filepath.Join(tmpDir, "versions.txt"), nil, 0644); err != nil {
t.Fatal(err)
}
if err := yaml.Write(config.LibrarianYAML, test.cfg); err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -449,6 +460,9 @@ func TestAddLibrary_ExistingLibrary_Error(t *testing.T) {
t.Run(test.name, func(t *testing.T) {
tmpDir := t.TempDir()
t.Chdir(tmpDir)
if err := os.WriteFile(filepath.Join(tmpDir, "versions.txt"), nil, 0644); err != nil {
t.Fatal(err)
}
if err := yaml.Write(config.LibrarianYAML, test.cfg); err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -593,6 +607,9 @@ func TestAddLibraryCommand_Java(t *testing.T) {
}
tmpDir := t.TempDir()
t.Chdir(tmpDir)
if err := os.WriteFile(filepath.Join(tmpDir, "versions.txt"), nil, 0644); err != nil {
t.Fatal(err)
}

cfg := sample.Config()
cfg.Language = config.LanguageJava
Expand Down
11 changes: 1 addition & 10 deletions internal/librarian/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,24 +237,15 @@ func generateLibraries(ctx context.Context, cfg *config.Config, libraries []*con
}
return g.Wait()
case config.LanguageJava:
var allMissingArtifacts []java.MissingArtifact
for _, library := range libraries {
missingArtifactIDs, err := java.IdentifyMissingModules(library, library.Output)
if err != nil {
return fmt.Errorf("failed to identify missing modules for %q: %w", library.Name, err)
}
for _, id := range missingArtifactIDs {
allMissingArtifacts = append(allMissingArtifacts, java.MissingArtifact{ID: id, Library: library})
}

if err := java.Generate(ctx, cfg, library, src); err != nil {
return fmt.Errorf("generate library %q (%s): %w", library.Name, cfg.Language, err)
}
if err := java.Format(ctx, library); err != nil {
return fmt.Errorf("format library %q (%s): %w", library.Name, cfg.Language, err)
}
}
return java.PostGenerate(ctx, ".", cfg, allMissingArtifacts)
return java.PostGenerate(ctx, ".", cfg)
case config.LanguageNodejs:
g, gctx := errgroup.WithContext(ctx)
for _, library := range libraries {
Expand Down
213 changes: 198 additions & 15 deletions internal/librarian/java/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,15 @@
package java

import (
"bytes"
"fmt"
"log"
"os"
"slices"
"strings"

"github.com/googleapis/librarian/internal/config"
"github.com/googleapis/librarian/internal/serviceconfig"
)

// knownPrefixes contains API path prefixes to be stripped when deriving a
Expand All @@ -35,40 +40,218 @@ const (
defaultVersion = "0.1.0-SNAPSHOT"
defaultReleasedVersion = "0.0.0"
fakeGroupID = "please-configure-java-group-id"
versionsFileName = "versions.txt"
)

// Add initializes a new Java library with default values.
func Add(lib *config.Library) *config.Library {
lib.Version = defaultVersion
// Add initializes a new Java library with default values, or extends an
// existing library with a new API path, and registers the appropriate
// modules in versions.txt.
func Add(lib *config.Library, addedAPI *config.API) (*config.Library, error) {
if lib.Version == "" {
lib.Version = defaultVersion
}
// Java generation defaults to the system year for license headers,
// so we reset it here to avoid redundancy in librarian.yaml.
lib.CopyrightYear = ""

if lib.Java == nil {
lib.Java = &config.JavaModule{}
}
lib.Java.ReleasedVersion = defaultReleasedVersion
if lib.Java.ReleasedVersion == "" && addedAPI == nil {
lib.Java.ReleasedVersion = defaultReleasedVersion
}

// We use the first API to infer the group ID.
// It is unrealistic for a single library to mix cloud and non-cloud APIs.
apiPath := lib.APIs[0].Path
switch {
case strings.HasPrefix(apiPath, "google/shopping/"):
Comment thread
noahdietz marked this conversation as resolved.
return setNonCloudMavenDefaults(lib, "com.google.shopping")
lib = setNonCloudMavenDefaults(lib, "com.google.shopping")
case strings.HasPrefix(apiPath, "google/maps/"):
return setNonCloudMavenDefaults(lib, "com.google.maps")
lib = setNonCloudMavenDefaults(lib, "com.google.maps")
case strings.HasPrefix(apiPath, "google/ads/"):
return setNonCloudMavenDefaults(lib, "com.google.api-ads")
lib = setNonCloudMavenDefaults(lib, "com.google.api-ads")
default:
if !strings.HasPrefix(apiPath, "google/cloud/") {
log.Printf(
"WARNING: unrecognized non-cloud API path %q. Setting fake GroupID %q. "+
"Please manually configure java.group_id and java.distribution_name_override in librarian.yaml.",
apiPath, fakeGroupID,
)
lib = setNonCloudMavenDefaults(lib, fakeGroupID)
}
}
if !strings.HasPrefix(apiPath, "google/cloud/") {
log.Printf(
"WARNING: unrecognized non-cloud API path %q. Setting fake GroupID %q. "+
"Please manually configure java.group_id and java.distribution_name_override in librarian.yaml.",
apiPath, fakeGroupID,
)
setNonCloudMavenDefaults(lib, fakeGroupID)

// Fill the library entry in order to derive the appropriate artifacts to add.
lib, err := Fill(lib)
if err != nil {
return nil, err
}
return lib

newArtifactIDs, err := deriveAddedArtifactIDs(lib, addedAPI)
if err != nil {
return nil, err
}
var versions []string
for _, id := range newArtifactIDs {
versions = append(versions, fmt.Sprintf("%s:%s:%s", id, lib.Java.ReleasedVersion, lib.Version))
}
if err := appendVersions(versions); err != nil {
return nil, err
}

lib, err = Tidy(lib)
if err != nil {
return nil, err
}
Comment thread
noahdietz marked this conversation as resolved.

return lib, nil
}

func deriveAddedArtifactIDs(lib *config.Library, addedAPI *config.API) ([]string, error) {
libCoord := deriveLibraryCoordinates(lib)
var modules []expectedModule

if addedAPI != nil {
Comment thread
noahdietz marked this conversation as resolved.
transport, err := serviceconfig.FindTransport(addedAPI.Path, config.LanguageJava)
if err != nil {
return nil, err
}
modules = expectedAPIModules(lib, addedAPI, libCoord, transport)
} else {
var err error
modules, err = expectedNewLibraryModules(lib)
if err != nil {
return nil, err
}
}

var artifacts []string
for _, m := range modules {
artifacts = append(artifacts, m.ArtifactID)
}

if lib.Java != nil && len(lib.Java.ExcludedPOMs) > 0 {
var filtered []string
for _, art := range artifacts {
if !slices.Contains(lib.Java.ExcludedPOMs, art) {
filtered = append(filtered, art)
}
}
artifacts = filtered
}

return artifacts, nil
}

// expectedNewLibraryModules returns all expected modules for a new library,
// ordered to match the versions.txt expectation: Parent, BOM, APIs, Client.
func expectedNewLibraryModules(lib *config.Library) ([]expectedModule, error) {
transports, err := loadTransports(lib)
if err != nil {
return nil, err
}
modules := expectedModules(lib, transports)

// Reorder modules to match versions.txt expectation: Parent, BOM, APIs, Client.
var parent *expectedModule
var bom *expectedModule
var client *expectedModule
var apis []expectedModule

for _, m := range modules {
switch m.Kind {
case kindParent:
parent = &m
case kindBOM:
bom = &m
case kindClient:
client = &m
default: // kindProto, kindGRPC
apis = append(apis, m)
}
}

var ordered []expectedModule
if parent != nil {
ordered = append(ordered, *parent)
}
if bom != nil {
ordered = append(ordered, *bom)
}
ordered = append(ordered, apis...)
if client != nil {
ordered = append(ordered, *client)
}
return ordered, nil
}

// readExistingModules reads and parses the versions file at the given path,
// returning a map of existing module artifact IDs to true.
func readExistingModules(path string) (map[string]bool, error) {
Comment thread
noahdietz marked this conversation as resolved.
content, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, err
}
modules := make(map[string]bool)
lines := bytes.Split(content, []byte("\n"))
for _, line := range lines {
line = bytes.TrimSpace(line)
if len(line) == 0 || bytes.HasPrefix(line, []byte("#")) {
continue
}
parts := bytes.Split(line, []byte(":"))
if len(parts) > 0 && len(parts[0]) > 0 {
modules[string(parts[0])] = true
}
}
return modules, nil
}
Comment thread
noahdietz marked this conversation as resolved.

func appendVersions(versions []string) error {
existing, err := readExistingModules(versionsFileName)
if err != nil {
return fmt.Errorf("failed to read %s: %w", versionsFileName, err)
}
var newVersions []string
for _, line := range versions {
parts := strings.Split(line, ":")
if len(parts) > 0 && !existing[parts[0]] {
newVersions = append(newVersions, line)
}
}
if err := appendLines(versionsFileName, newVersions); err != nil {
return fmt.Errorf("failed to update %s: %w", versionsFileName, err)
}
return nil
}

// appendLines appends the given lines to an existing file, ensuring that it
// ends with a newline character before appending. It returns an error if the
// file does not exist.
func appendLines(path string, lines []string) error {
if len(lines) == 0 {
return nil
}
existing, err := os.ReadFile(path)
if err != nil {
return err
}
var buf bytes.Buffer
buf.Write(existing)
// Ensure the file ends with a newline before appending so that we
// do not concatenate lines instead of appending them.
if len(existing) > 0 && existing[len(existing)-1] != '\n' {
buf.WriteByte('\n')
}
for _, line := range lines {
buf.WriteString(line)
buf.WriteByte('\n')
}
return os.WriteFile(path, buf.Bytes(), 0644)
}

func setNonCloudMavenDefaults(lib *config.Library, groupID string) *config.Library {
Expand Down
Loading
Loading