Skip to content

feat: Jfrog CLI for Artifactory SDK targets - #168

Open
MEverett90 wants to merge 6 commits into
mainfrom
me/yocto-sdk-artifactory
Open

feat: Jfrog CLI for Artifactory SDK targets#168
MEverett90 wants to merge 6 commits into
mainfrom
me/yocto-sdk-artifactory

Conversation

@MEverett90

Copy link
Copy Markdown
Collaborator

Description

For configured Yocto sdk_url that points to an Artifactory location, verifies jfrog CLI is available and uses it to download the SDK.

Type of Change

  • ✨ New feature (non-breaking change which adds functionality)
  • 🛠️ Bug fix (non-breaking change which fixes an issue)
  • ❌ Breaking change (fix or feature that would cause existing functionality to change)
  • 🧹 Code refactor
  • ✅ Build configuration change
  • 📝 Documentation
  • 🗑️ Chore

@MEverett90 MEverett90 changed the title Jfrog CLI for Artifactory SDK targets feat: Jfrog CLI for Artifactory SDK targets Aug 26, 2026
@MEverett90
MEverett90 force-pushed the me/yocto-sdk-artifactory branch from 9702dca to 7dd650a Compare August 26, 2026 14:39
@MEverett90
MEverett90 requested a review from jwinarske August 26, 2026 14:52

@jwinarske jwinarske left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sh -> direct-exec change is correct — chmod +x already runs just above at line 220, so the shebang is honored — and threading a real reason into the unavailable message is a solid UX improvement over the bare "no environment-setup-* found". Three issues in the new jf path, inline.

One more small thing not worth its own thread: _downloadError is never cleared, so if resolve() is ever called twice on the same provider instance a stale reason can leak into an unrelated failure message.

Comment thread lib/src/cross/yocto_sdk_cross_provider.dart Outdated
Comment thread lib/src/cross/yocto_sdk_cross_provider.dart Outdated
Comment thread lib/src/cross/yocto_sdk_cross_provider.dart
…, select jf server-id by URL authority, fall through to HTTP when jf absent

- Clear _downloadError and _artifacts at the top of resolve() so stale
  state from a prior call never leaks into an unrelated error message or
  lock entry.
- _downloadViaJFrog now returns bool? (null = fall through to plain HTTP)
  and returns null in all cases where jf cannot be used: CLI not installed,
  `jf config show` fails, or no configured server matches the Artifactory
  URL's authority.
- Resolves the correct --server-id by matching the Artifactory URL's
  authority against `jf config show` output, so multi-server setups pick
  the right credentials rather than silently using the default.
- Adds --fail-no-op to jf rt dl and a post-download existence check so a
  silent mismatch between the URL basename and the repository artifact name
  produces a clear error instead of a missing-file mystery later.
- Fixes _parseArtifactoryPath to use uri.authority (preserves port) instead
  of uri.host.
- Drop redundant jf --version probe; jf config show already throws
  ProcessException when jf is absent.
- _parseArtifactoryPath now returns (authority, repoPath) directly,
  removing the dead basePath construction and the Uri.parse round-trip
  in _downloadViaJFrog.
- Extract _failMsg helper to deduplicate the stderr-formatting pattern
  shared by _materializeFromUrl and _downloadViaJFrog.
- Parse the installer URL once in _materializeFromUrl instead of twice.
- Remove no-op value.trim() in _parseJFrogServers (line already trimmed
  before the regex match).
@jwinarske

Copy link
Copy Markdown
Contributor

Good iteration. Confirming what the last two commits closed:

  • _downloadError / _artifacts are reset per resolve() — no stale reason leaking.
  • Anonymous Artifactory no longer hard-fails: _downloadViaJFrog returns null when jf is missing or unconfigured, and _download falls through to plain HTTP.
  • The wrong-server hazard is fixed better than I suggested — matching Artifactory URL authority to pick --server-id beats --url, and since Uri.authority includes the port, the https://host:8081/... case I flagged is handled for free.
  • --fail-no-op and the dest.existsSync() check — belt and suspenders, and the second one catches the case where the artifact's name in the repo differs from the URL basename.

1. Still open from last round: the /artifactory/api/download/<repo>/... URL form.

_parseArtifactoryPath (line 359) returns api/download/<repo>/... as repoPath, which is not a valid jf rt dl path — jf reads api as the repository name and matches nothing.

This is worse now than when I first raised it, because the failure is no longer recoverable. --fail-no-op makes jf exit non-zero, _downloadViaJFrog returns false rather than null, and _download (line 253) returns that false straight out without ever trying HTTP. So an sdk_url in the api/download form — which plain HTTPS handled fine before this PR — now fails outright with jf rt dl failed. Strip the api/download prefix when present before building repoPath.

2. Process.run(installer.path, ...) at line 227 can throw rather than return non-zero.

I said last time the sh -> direct-exec swap was safe because chmod +x runs above it — that holds for the permission bit, but not for the failure path. chmod's ProcessResult is discarded (line 221), and if the exec cannot start at all — chmod failed, or the workspace is on a noexec mount, both of which happen in CI — Dart raises ProcessException, and nothing in _materializeFromUrl or resolve() catches it. Under sh that was an exit code; now it is an uncaught crash, which is the opposite of what this PR is for. Wrap it:

final ProcessResult run;
try {
  run = await Process.run(installer.path, ['-y', '-d', prefix.path]);
} on ProcessException catch (e) {
  _downloadError = 'cannot execute ${installer.path}: ${e.message}';
  return null;
}

3. jf installed but no server matches -> the fallback reports the wrong cause.

Keeping the HTTP fallback is right, but when it fires because no configured server matches the authority (line 301), the user sees HTTP 401 downloading <url> when the actual cause is a missing jf c add. That is the most likely first-run failure for the case this PR exists to serve. Set the hint before returning null, and let the HTTP attempt overwrite it if it produces its own error:

_downloadError = 'no jf server configured for $baseAuthority — run `jf c add`';
return null;

4. A truncated installer persists and is then executed.

await resp.pipe(dest.openWrite()) (line 265): if the connection drops mid-body, the partial file stays on disk. The next run hits installer.existsSync() at line 209, skips the download, hashes the truncated file into emb.lock, and runs it. Pre-existing, but this PR is rewriting the function and adding the error plumbing that makes it easy to handle — download to <name>.part, rename on success, and delete the partial in the catch.

5. _parseJFrogServers and _parseArtifactoryPath are pure statics with no tests.

test/src/cross/ has roughly thirty test files; these two stand out, and the first parses a CLI's human-readable output — a format that can change under us on a jf upgrade, silently degrading every Artifactory download to an unauthenticated 401. Mark both @visibleForTesting and pin a real multi-server jf config show sample, plus the URL forms (plain, api/download, with port, no /artifactory/ segment).

Nits:

  • _downloadError now also carries installer failures (line 229), so the name undersells it — _failureDetail reads truer.
  • '${dest.parent.path}/' (line 311) hardcodes the separator. Yocto SDKs are Linux-only in practice, but the repo does run Windows CI.
  • "honoured" in the comment at line 225 is British spelling; this repo is US English and there is a spell-check job on every PR.

…aths, and parser tests

Download to a .part file and rename on success so a mid-stream connection
drop cannot leave a truncated installer that silently re-used on the next
run. Strip the api/download prefix from Artifactory paths before passing
them to jf rt dl, fixing a regression where those URLs failed hard instead
of falling back to HTTP. Expose parseArtifactoryPath and parseJFrogServers
as @VisibleForTesting statics (with meta dep) and add tests covering plain,
api/download, ported, and no-artifactory URL forms plus a real multi-server
jf config show sample.
@MEverett90

Copy link
Copy Markdown
Collaborator Author

@jwinarske Thanks for the follow-up, concerns have been addressed in latest commit, please re-review at your earliest convenience.

@jwinarske jwinarske left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MEverett90

Read through the latest. The shape is good — resolving --server-id by matching
the URL authority against jf config show rather than assuming the default
server is the right call, the .part staging on the HTTP path is a real
improvement, and parseArtifactoryPath / parseJFrogServers being pure and
@visibleForTesting makes the tricky parts testable without a network. Running
the installer directly instead of through sh is correct, and chmod +x above
it already covers the exec bit.

Two things I would change, plus one consideration.

1. _failureDetail survives a successful fallback and can mislead

_downloadViaJFrog sets

_failureDetail = "no jf server configured for $baseAuthority — run `jf c add`";
return null;

then returns null so the caller falls through to plain HTTP. _download never
clears it on the success path, and resolve() only reads it much later.

So: Artifactory URL, jf installed but no server matching that authority, HTTP
fallback downloads fine, installer runs, and the SDK layout turns out not to
have an environment-setup-* where expected. The user gets

no environment-setup-* found (no jf server configured for artifacts.example.com
— run `jf c add`) — set cross.sdk_path to ...

which points at JFrog config that was not the problem and had already been
routed around. One line fixes it — clear _failureDetail on the success paths
in _download, so a detail only ever describes the failure actually being
reported.

2. The jf path lacks the atomicity the HTTP path just gained

The HTTP path now writes ${dest.path}.part and renames, which is exactly the
right fix. jf rt dl --flat writes into dest.parent directly, so an
interrupted download (Ctrl-C, runner timeout, disk full) can leave a truncated
file at dest.path.

The next run then takes the installer.existsSync() branch, skips the download,
records that partial file's sha256 in emb.lock, chmods it and executes it. The
best case is a confusing installer failure; the worse case is a pinned lock
entry for a corrupt artifact. Downloading into a temp directory and renaming
into place would give jf the same guarantee — and it matters more here, since
the existing-file check makes a partial download sticky rather than
self-correcting.

3. Consideration: _failMsg puts subprocess stderr into a user-facing error

jf rt dl stderr can include the resolved URL, and depending on configuration
that can carry an access token. Worth deciding deliberately whether to pass it
through, truncate it, or scrub anything URL-shaped — not a blocker, just easy to
overlook until it lands in someone's CI log.

One thing I want to confirm rather than assume: a jf failure returns false
and deliberately does not fall back to HTTP, while jf being unusable returns
null and does. That reads intentional — falling back to unauthenticated HTTP
after an auth failure would just 401, or worse quietly fetch something else —
and the doc comment says so. Flagging only because the asymmetry is load-bearing
and easy to "simplify" away later.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants