From patchwork Wed Mar 25 06:51:43 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: AdrianF X-Patchwork-Id: 84319 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id AA935FEA82C for ; Wed, 25 Mar 2026 07:14:12 +0000 (UTC) Received: from mta-64-226.siemens.flowmailer.net (mta-64-226.siemens.flowmailer.net [185.136.64.226]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.17081.1774422847651794276 for ; Wed, 25 Mar 2026 00:14:09 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=adrian.freihofer@siemens.com header.s=fm1 header.b=eYULxNBk; spf=pass (domain: rts-flowmailer.siemens.com, ip: 185.136.64.226, mailfrom: fm-1329275-20260325071404843bff407c00020798-ybzcwt@rts-flowmailer.siemens.com) Received: by mta-64-226.siemens.flowmailer.net with ESMTPSA id 20260325071404843bff407c00020798 for ; Wed, 25 Mar 2026 08:14:05 +0100 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; s=fm1; d=siemens.com; i=adrian.freihofer@siemens.com; h=Date:From:Subject:To:Message-ID:MIME-Version:Content-Type:Content-Transfer-Encoding:Cc:References:In-Reply-To; bh=8O+FyGoyAEK3XM0zpeEfEsfhcm7A85XV6lfCA+ddus0=; b=eYULxNBkKagr6qsJv6bHTEUK3A1F8TOTYn/9XYyGmNZZ3Mgb4EHFLtTT3iaw0xqS2kVJv4 wTMWibVWNaFMgX85NmUwljXUgoE8h82Ba1O2llNSOtRwUg2SLy0obc4Qoh8nDAGQTeK3M7hU Ps5BgkOz+hSHVaxYeGQYwp2f6HxAUtHzPCv1nUEftw2uQprkAWWPzzgqesTm9qvJfqlt9Ec6 LoX4JPXwWt+hceJcNl4j9zwiznGUSqVPsL1ffxC5vQmcidsoqUQtIgAeXGW9Xtqjo7IiqdI2 hofG+fzF590TXY+2Tt37gvAQKsVuaMbzAXMVYDNFQEvVN8ok35xqbG/w==; From: AdrianF To: bitbake-devel@lists.openembedded.org Cc: Adrian Freihofer Subject: [PATCH v2 01/10] fetch2: add LocalModificationsError for uncommitted changes blocking update Date: Wed, 25 Mar 2026 07:51:43 +0100 Message-ID: <20260325071342.47272-2-adrian.freihofer@siemens.com> In-Reply-To: <20260325071342.47272-1-adrian.freihofer@siemens.com> References: <20260325071342.47272-1-adrian.freihofer@siemens.com> MIME-Version: 1.0 X-Flowmailer-Platform: Siemens Feedback-ID: 519:519-1329275:519-21489:flowmailer List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Wed, 25 Mar 2026 07:14:12 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/bitbake-devel/message/19228 From: Adrian Freihofer When unpack_update() detects uncommitted changes in a layer repository it now raises LocalModificationsError, a new subclass of UnpackError, instead of a plain UnpackError. The new exception: - takes (repodir, url, git_output) as arguments - builds the full user-facing message in its __init__, combining the git status output with guidance to commit, stash or discard changes - keeps the is-a relationship with UnpackError so existing callers are not broken Signed-off-by: Adrian Freihofer --- lib/bb/fetch2/__init__.py | 8 ++++++++ lib/bb/fetch2/git.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/bb/fetch2/__init__.py b/lib/bb/fetch2/__init__.py index 909ae9814..b39cfcd45 100644 --- a/lib/bb/fetch2/__init__.py +++ b/lib/bb/fetch2/__init__.py @@ -96,6 +96,14 @@ class UnpackError(BBFetchException): BBFetchException.__init__(self, msg) self.args = (message, url) +class LocalModificationsError(UnpackError): + """Exception raised when a checkout cannot be updated due to local modifications""" + def __init__(self, repodir, url, git_output): + message = ("Repository at %s has uncommitted changes, unable to update:\n%s\n" + "Please commit, stash or discard your changes and re-run the update." % (repodir, git_output)) + UnpackError.__init__(self, message, url) + self.args = (repodir, url, git_output) + class NoMethodError(BBFetchException): """Exception raised when there is no method to obtain a supplied url or set of urls""" def __init__(self, url): diff --git a/lib/bb/fetch2/git.py b/lib/bb/fetch2/git.py index 84fecdad0..c398f7e9d 100644 --- a/lib/bb/fetch2/git.py +++ b/lib/bb/fetch2/git.py @@ -730,7 +730,7 @@ class Git(FetchMethod): output = runfetchcmd("%s status --untracked-files=no --porcelain" % (ud.basecmd), d, workdir=destdir) if output: - raise bb.fetch2.UnpackError("Repository at %s has uncommitted changes, unable to update:\n%s" % (destdir, output), ud.url) + raise bb.fetch2.LocalModificationsError(destdir, ud.url, output) # Set up remote for the download location if it doesn't exist try: From patchwork Wed Mar 25 06:51:44 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: AdrianF X-Patchwork-Id: 84310 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id E8DF3FEA81A for ; Wed, 25 Mar 2026 07:14:10 +0000 (UTC) Received: from mta-65-225.siemens.flowmailer.net (mta-65-225.siemens.flowmailer.net [185.136.65.225]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.17082.1774422847651944667 for ; Wed, 25 Mar 2026 00:14:09 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=adrian.freihofer@siemens.com header.s=fm1 header.b=e5KMPaMG; spf=pass (domain: rts-flowmailer.siemens.com, ip: 185.136.65.225, mailfrom: fm-1329275-20260325071404aa370c328100020791-z8avl6@rts-flowmailer.siemens.com) Received: by mta-65-225.siemens.flowmailer.net with ESMTPSA id 20260325071404aa370c328100020791 for ; Wed, 25 Mar 2026 08:14:05 +0100 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; s=fm1; d=siemens.com; i=adrian.freihofer@siemens.com; h=Date:From:Subject:To:Message-ID:MIME-Version:Content-Type:Content-Transfer-Encoding:Cc:References:In-Reply-To; bh=HQkO8pRCxFX5GjFeHSMGuIDODG4BzWMPBjQTKYu+icg=; b=e5KMPaMGpOsJeA4WUnIFWud9YtzpLit+tdzq7a4a1H+if6zfFSbqYK1G9Rh4FfVgAGSnRp YHwrKAQMBN38NhkisskGwDO5UxrDLc7Plb6MR9SJQrfW0c1LlnoypAYv6HB2GT35FbzurtGG uykEqlwD1UMNlUfyzODSQAM4+I+Y7Ipmurvsu8tvgbPXkhfAQw8R4/4wG1prfVjfsfN5gSgl F8scNcn2LnuXtMn6oFGYEs3rvVd8JX49z8fsAnMH8FIVZXMdKSgRSbONMxpy9xt9j08YG6NU vQHkpLP4dwlJFKZ2CeBWjO37HgSf36lCZBYc7CxXTrXFOLZy6i/itbTA==; From: AdrianF To: bitbake-devel@lists.openembedded.org Cc: Adrian Freihofer Subject: [PATCH v2 02/10] bitbake-selftest: add GitUnpackUpdateTest Date: Wed, 25 Mar 2026 07:51:44 +0100 Message-ID: <20260325071342.47272-3-adrian.freihofer@siemens.com> In-Reply-To: <20260325071342.47272-1-adrian.freihofer@siemens.com> References: <20260325071342.47272-1-adrian.freihofer@siemens.com> MIME-Version: 1.0 X-Flowmailer-Platform: Siemens Feedback-ID: 519:519-1329275:519-21489:flowmailer List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Wed, 25 Mar 2026 07:14:10 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/bitbake-devel/message/19230 From: Adrian Freihofer Add a test class that exercises the new unpack_update() code path in the Git fetcher, ordered from basic building blocks to advanced workflow and error cases: test_unpack_update_full_clone Basic update to a newer upstream revision succeeds and the working tree reflects the new content. test_unpack_update_dldir_remote_setup The "dldir" remote pointing to ud.clonedir is created during the initial unpack and is present for subsequent update calls. test_unpack_update_ff_with_local_changes Full workflow: after a normal unpack the "dldir" remote is verified, a local commit is added, download() brings updated_rev into the clonedir, and unpack_update() fetches from dldir and rebases the local commit fast forward on top. The commit graph (HEAD^ == updated_rev) and both file contents are asserted. test_unpack_update_already_at_target_revision Calling unpack_update() when the checkout is already at SRCREV is a no-op: it succeeds and the working tree is left unchanged. test_unpack_update_with_untracked_file The status check uses --untracked-files=no so untracked files are invisible to it; the update succeeds and the untracked file survives the rebase unchanged. test_unpack_update_with_staged_changes Staged (but not committed) changes cause git to refuse to rebase under --no-autostash; UnpackError is raised so the caller can fall back to backup + re-fetch. test_unpack_update_with_modified_tracked_file An unstaged modification to a tracked file is detected by "git status --untracked-files=no --porcelain" and blocks the update; UnpackError is raised. test_unpack_update_conflict_raises_unpack_error A local commit that conflicts with the incoming upstream change raises UnpackError; the repository is left in a clean state with no pending rebase (rebase --abort was called). test_unpack_update_untracked_file_overwritten_by_upstream An untracked file that would be overwritten by an incoming upstream commit causes git to refuse the rebase; UnpackError is raised and the repository is not left in a mid-rebase state. Two sub-cases are covered: a top-level file clash and a clash inside a subdirectory (xxx/somefile). test_unpack_update_shallow_clone_fails Shallow clones do not carry enough history; UnpackError is raised. test_unpack_update_stale_dldir_remote When the clonedir has been removed after the initial unpack the dldir remote no longer resolves; UnpackError is raised so the caller can fall back to a full re-fetch. test_fetch_unpack_update_toplevel_api The public Fetch.unpack_update(root) API (used by callers such as bitbake-setup) dispatches correctly end-to-end through to the Git fetcher. Signed-off-by: Adrian Freihofer --- lib/bb/tests/fetch.py | 556 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 556 insertions(+) diff --git a/lib/bb/tests/fetch.py b/lib/bb/tests/fetch.py index 7b8297a78..25432e030 100644 --- a/lib/bb/tests/fetch.py +++ b/lib/bb/tests/fetch.py @@ -3793,3 +3793,559 @@ class GoModGitTest(FetcherTest): self.assertTrue(os.path.exists(os.path.join(downloaddir, 'go.opencensus.io/@v/v0.24.0.mod'))) self.assertEqual(bb.utils.sha256_file(os.path.join(downloaddir, 'go.opencensus.io/@v/v0.24.0.mod')), '0dc9ccc660ad21cebaffd548f2cc6efa27891c68b4fbc1f8a3893b00f1acec96') + + +class GitUnpackUpdateTest(FetcherTest): + """Test the unpack_update functionality for git fetcher. + + Intended workflow + 1. First-time setup: + 1. download() — clones the upstream repo into DL_DIR/git2/... (clonedir). + 2. unpack() — clones from clonedir into the workspace (S/workdir) and + registers a 'dldir' git remote pointing at + file://DL_DIR/git2/... for later offline use. + + 2. Subsequent updates (what unpack_update is designed for): + 1. The user works in the unpacked source tree. + 2. Upstream advances — SRCREV changes in the recipe. + 3. download() — fetches the new revision into the local clonedir. + 4. unpack_update() — instead of wiping the workspace and re-cloning: + * fetches the new revision from the local 'dldir' remote + * rebases the user's local commits on top of the new SRCREV + * raises UnpackError if anything prevents a clean rebase so the + caller (e.g. bitbake-setup) can fall back to backup + re-clone. + + Key design constraints: + * unpack_update() never deletes existing data (unlike unpack()). + * Only staged/modified tracked files block the update; untracked files and + committed local work are handled gracefully. + * The 'dldir' remote is intentionally visible to users outside the + fetcher (e.g. for manual 'git log dldir/master'). + * Currently only git is supported. + """ + + def setUp(self): + """Set up a local bare git source repository with two commits on 'master'. + + self.initial_rev — the first commit (testfile.txt: 'initial content') + self.updated_rev — the second commit (testfile.txt: 'updated content') + + SRCREV is initialised to self.initial_rev so individual tests can + advance it to self.updated_rev (or create further commits) as needed. + """ + FetcherTest.setUp(self) + + self.gitdir = os.path.join(self.tempdir, 'gitrepo') + self.srcdir = os.path.join(self.tempdir, 'gitsource') + + self.d.setVar('WORKDIR', self.tempdir) + self.d.setVar('S', self.gitdir) + self.d.delVar('PREMIRRORS') + self.d.delVar('MIRRORS') + + # Create a source git repository + bb.utils.mkdirhier(self.srcdir) + self.git_init(cwd=self.srcdir) + + # Create initial commit + with open(os.path.join(self.srcdir, 'testfile.txt'), 'w') as f: + f.write('initial content\n') + self.git(['add', 'testfile.txt'], cwd=self.srcdir) + self.git(['commit', '-m', 'Initial commit'], cwd=self.srcdir) + self.initial_rev = self.git(['rev-parse', 'HEAD'], cwd=self.srcdir).strip() + + # Create a second commit + with open(os.path.join(self.srcdir, 'testfile.txt'), 'w') as f: + f.write('updated content\n') + self.git(['add', 'testfile.txt'], cwd=self.srcdir) + self.git(['commit', '-m', 'Update commit'], cwd=self.srcdir) + self.updated_rev = self.git(['rev-parse', 'HEAD'], cwd=self.srcdir).strip() + + self.d.setVar('SRCREV', self.initial_rev) + self.d.setVar('SRC_URI', 'git://%s;branch=master;protocol=file' % self.srcdir) + + def test_unpack_update_full_clone(self): + """Test that unpack_update updates an existing checkout in place for a full clone. + + Steps: + 1. Fetch and unpack at self.initial_rev — verify 'initial content'. + 2. Advance SRCREV to self.updated_rev and re-download. + 3. Call unpack_update() instead of unpack() — the existing checkout + must be updated via 'git fetch dldir' + 'git rebase' without + re-cloning the directory. + 4. Verify testfile.txt now contains 'updated content'. + """ + # First fetch at initial revision + fetcher = bb.fetch2.Fetch([self.d.getVar('SRC_URI')], self.d) + fetcher.download() + fetcher.unpack(self.unpackdir) + + # Verify initial state + unpack_path = os.path.join(self.unpackdir, 'git') + self.assertTrue(os.path.exists(os.path.join(unpack_path, 'testfile.txt'))) + with open(os.path.join(unpack_path, 'testfile.txt'), 'r') as f: + self.assertEqual(f.read(), 'initial content\n') + + # Update to new revision + self.d.setVar('SRCREV', self.updated_rev) + fetcher = bb.fetch2.Fetch([self.d.getVar('SRC_URI')], self.d) + fetcher.download() + + # Use unpack_update + uri = self.d.getVar('SRC_URI') + ud = fetcher.ud[uri] + git_fetcher = ud.method + git_fetcher.unpack_update(ud, self.unpackdir, self.d) + + # Verify updated state + with open(os.path.join(unpack_path, 'testfile.txt'), 'r') as f: + self.assertEqual(f.read(), 'updated content\n') + + def test_unpack_update_dldir_remote_setup(self): + """Test that unpack() adds a 'dldir' git remote pointing at ud.clonedir. + + The 'dldir' remote is used by subsequent unpack_update() calls to fetch + new commits from the local download cache (${DL_DIR}/git2/…) without + requiring network access. After a normal unpack the remote must exist + and its URL must be 'file://'. + """ + # First fetch + fetcher = bb.fetch2.Fetch([self.d.getVar('SRC_URI')], self.d) + fetcher.download() + + uri = self.d.getVar('SRC_URI') + ud = fetcher.ud[uri] + + fetcher.unpack(self.unpackdir) + + unpack_path = os.path.join(self.unpackdir, 'git') + + # Check that dldir remote exists + remotes = self.git(['remote'], cwd=unpack_path).strip().split('\n') + self.assertIn('dldir', remotes) + + # Verify it points to the clonedir + dldir_url = self.git(['remote', 'get-url', 'dldir'], cwd=unpack_path).strip() + self.assertEqual(dldir_url, 'file://{}'.format(ud.clonedir)) + + def test_unpack_update_ff_with_local_changes(self): + """Test that unpack_update rebases local commits fast forward. + + Full workflow: + 1. Fetch + unpack at initial_rev — verify 'dldir' remote is created + pointing at ud.clonedir. + 2. Add a local commit touching localfile.txt. + 3. Advance SRCREV to updated_rev and call download() — verify that + ud.clonedir (the dldir bare clone) now contains updated_rev. + 4. Call unpack_update() — it fetches updated_rev from dldir into the + working tree and rebases the local commit on top. + 5. Verify the final commit graph: HEAD's parent is updated_rev, and + both testfile.txt ('updated content') and localfile.txt ('local + change') are present. + + Note: git rebase operates the same way regardless of whether HEAD is + detached or on a named branch (e.g. 'master' or a local feature branch), + so this test covers those scenarios implicitly. + """ + # Step 1 — fetch + unpack at initial_rev + fetcher = bb.fetch2.Fetch([self.d.getVar('SRC_URI')], self.d) + fetcher.download() + fetcher.unpack(self.unpackdir) + + uri = self.d.getVar('SRC_URI') + ud = fetcher.ud[uri] + unpack_path = os.path.join(self.unpackdir, 'git') + + # The normal unpack must have set up the 'dldir' remote pointing at + # ud.clonedir so that subsequent unpack_update() calls work offline. + dldir_url = self.git(['remote', 'get-url', 'dldir'], cwd=unpack_path).strip() + self.assertEqual(dldir_url, 'file://{}'.format(ud.clonedir)) + + # Step 2 — add a local commit that touches a new file + with open(os.path.join(unpack_path, 'localfile.txt'), 'w') as f: + f.write('local change\n') + self.git(['add', 'localfile.txt'], cwd=unpack_path) + self.git(['commit', '-m', 'Local commit'], cwd=unpack_path) + local_commit = self.git(['rev-parse', 'HEAD'], cwd=unpack_path).strip() + + # Step 3 — advance SRCREV and download; clonedir must now contain + # updated_rev so that unpack_update can fetch it without network access. + self.d.setVar('SRCREV', self.updated_rev) + fetcher = bb.fetch2.Fetch([self.d.getVar('SRC_URI')], self.d) + fetcher.download() + + ud = fetcher.ud[uri] + clonedir_refs = self.git(['rev-parse', self.updated_rev], cwd=ud.clonedir).strip() + self.assertEqual(clonedir_refs, self.updated_rev, + "clonedir must contain updated_rev after download()") + + # Step 4 — unpack_update fetches from dldir and rebases + git_fetcher = ud.method + git_fetcher.unpack_update(ud, self.unpackdir, self.d) + + # Step 5 — verify the commit graph and working tree content + # HEAD is the rebased local commit; its parent must be updated_rev + head_rev = self.git(['rev-parse', 'HEAD'], cwd=unpack_path).strip() + parent_rev = self.git(['rev-parse', 'HEAD^'], cwd=unpack_path).strip() + self.assertNotEqual(head_rev, local_commit, + "local commit should have a new SHA after rebase") + self.assertEqual(parent_rev, self.updated_rev, + "HEAD's parent must be updated_rev after fast-forward rebase") + + with open(os.path.join(unpack_path, 'testfile.txt'), 'r') as f: + self.assertEqual(f.read(), 'updated content\n') + with open(os.path.join(unpack_path, 'localfile.txt'), 'r') as f: + self.assertEqual(f.read(), 'local change\n') + + def test_unpack_update_already_at_target_revision(self): + """Test that unpack_update is a no-op when the checkout is already at SRCREV. + + Calling unpack_update() without advancing SRCREV must succeed and leave + the working tree unchanged. No rebase should be attempted because the + checkout already points at ud.revision. + """ + fetcher = bb.fetch2.Fetch([self.d.getVar('SRC_URI')], self.d) + fetcher.download() + fetcher.unpack(self.unpackdir) + + unpack_path = os.path.join(self.unpackdir, 'git') + with open(os.path.join(unpack_path, 'testfile.txt')) as f: + self.assertEqual(f.read(), 'initial content\n') + + # Call unpack_update with SRCREV still at initial_rev — no upstream change + uri = self.d.getVar('SRC_URI') + ud = fetcher.ud[uri] + git_fetcher = ud.method + result = git_fetcher.unpack_update(ud, self.unpackdir, self.d) + self.assertTrue(result) + + # Content must be unchanged + with open(os.path.join(unpack_path, 'testfile.txt')) as f: + self.assertEqual(f.read(), 'initial content\n') + + def test_unpack_update_with_untracked_file(self): + """Test that unpack_update succeeds when the checkout has an untracked file. + + The status check uses '--untracked-files=no', so untracked files are not + detected and do not trigger the fallback path. git rebase also leaves + untracked files untouched, so both the upstream update and the untracked + file must be present after the call. + """ + fetcher = bb.fetch2.Fetch([self.d.getVar('SRC_URI')], self.d) + fetcher.download() + fetcher.unpack(self.unpackdir) + + unpack_path = os.path.join(self.unpackdir, 'git') + + # Create an untracked file (not staged, not committed) + untracked = os.path.join(unpack_path, 'untracked.txt') + with open(untracked, 'w') as f: + f.write('untracked content\n') + + # Update to new upstream revision + self.d.setVar('SRCREV', self.updated_rev) + fetcher = bb.fetch2.Fetch([self.d.getVar('SRC_URI')], self.d) + fetcher.download() + + uri = self.d.getVar('SRC_URI') + ud = fetcher.ud[uri] + git_fetcher = ud.method + + # --untracked-files=no means the status check passes; rebase preserves the file + git_fetcher.unpack_update(ud, self.unpackdir, self.d) + + with open(os.path.join(unpack_path, 'testfile.txt'), 'r') as f: + self.assertEqual(f.read(), 'updated content\n') + + # Untracked file must survive the rebase + self.assertTrue(os.path.exists(untracked)) + with open(untracked, 'r') as f: + self.assertEqual(f.read(), 'untracked content\n') + + def test_unpack_update_with_staged_changes(self): + """Test that unpack_update fails when the checkout has staged (but not committed) changes. + + The rebase is run with --no-autostash so git refuses to rebase over a + dirty index. The caller (bitbake-setup) is expected to catch the + resulting LocalModificationsError and fall back to backup + re-fetch. + """ + fetcher = bb.fetch2.Fetch([self.d.getVar('SRC_URI')], self.d) + fetcher.download() + fetcher.unpack(self.unpackdir) + + unpack_path = os.path.join(self.unpackdir, 'git') + + # Stage a new file without committing it + staged = os.path.join(unpack_path, 'staged.txt') + with open(staged, 'w') as f: + f.write('staged content\n') + self.git(['add', 'staged.txt'], cwd=unpack_path) + + # Update to new upstream revision + self.d.setVar('SRCREV', self.updated_rev) + fetcher = bb.fetch2.Fetch([self.d.getVar('SRC_URI')], self.d) + fetcher.download() + + uri = self.d.getVar('SRC_URI') + ud = fetcher.ud[uri] + git_fetcher = ud.method + + # Should fail — git rebase refuses to run with a dirty index + with self.assertRaises(bb.fetch2.LocalModificationsError): + git_fetcher.unpack_update(ud, self.unpackdir, self.d) + + def test_unpack_update_with_modified_tracked_file(self): + """Test that unpack_update fails when a tracked file has unstaged modifications. + + 'git status --untracked-files=no --porcelain' reports unstaged modifications + to tracked files (output line ' M filename'), which must block the update so + the caller can fall back to backup + re-fetch rather than silently discarding + work in progress. + """ + fetcher = bb.fetch2.Fetch([self.d.getVar('SRC_URI')], self.d) + fetcher.download() + fetcher.unpack(self.unpackdir) + + unpack_path = os.path.join(self.unpackdir, 'git') + + # Modify a tracked file without staging or committing + with open(os.path.join(unpack_path, 'testfile.txt'), 'w') as f: + f.write('locally modified content\n') + + # Update to new upstream revision + self.d.setVar('SRCREV', self.updated_rev) + fetcher = bb.fetch2.Fetch([self.d.getVar('SRC_URI')], self.d) + fetcher.download() + + uri = self.d.getVar('SRC_URI') + ud = fetcher.ud[uri] + git_fetcher = ud.method + + # Should fail — unstaged modification to tracked file is detected by + # 'git status --untracked-files=no --porcelain' + with self.assertRaises(bb.fetch2.LocalModificationsError): + git_fetcher.unpack_update(ud, self.unpackdir, self.d) + + def test_unpack_update_conflict_raises_unpack_error(self): + """Test that unpack_update raises UnpackError on a rebase conflict. + + When a local commit modifies the same lines as an incoming upstream commit, + git rebase cannot resolve the conflict automatically. unpack_update must + abort the failed rebase and raise UnpackError so the caller can fall back + to a backup + re-fetch. + """ + # Fetch and unpack at the initial revision + fetcher = bb.fetch2.Fetch([self.d.getVar('SRC_URI')], self.d) + fetcher.download() + fetcher.unpack(self.unpackdir) + + unpack_path = os.path.join(self.unpackdir, 'git') + + # Make a local commit that edits the same lines as the upcoming upstream commit + with open(os.path.join(unpack_path, 'testfile.txt'), 'w') as f: + f.write('conflicting local content\n') + self.git(['add', 'testfile.txt'], cwd=unpack_path) + self.git(['commit', '-m', 'Local conflicting commit'], cwd=unpack_path) + + # Add a third upstream commit that also edits testfile.txt differently + with open(os.path.join(self.srcdir, 'testfile.txt'), 'w') as f: + f.write('conflicting upstream content\n') + self.git(['add', 'testfile.txt'], cwd=self.srcdir) + self.git(['commit', '-m', 'Upstream conflicting commit'], cwd=self.srcdir) + conflict_rev = self.git(['rev-parse', 'HEAD'], cwd=self.srcdir).strip() + + # Update SRCREV to the new upstream commit + self.d.setVar('SRCREV', conflict_rev) + fetcher = bb.fetch2.Fetch([self.d.getVar('SRC_URI')], self.d) + fetcher.download() + + uri = self.d.getVar('SRC_URI') + ud = fetcher.ud[uri] + git_fetcher = ud.method + + # unpack_update must fail and clean up (rebase --abort) rather than + # leaving the repo in a mid-rebase state + with self.assertRaises(bb.fetch2.UnpackError): + git_fetcher.unpack_update(ud, self.unpackdir, self.d) + + # Verify the repo is not left in a conflicted / mid-rebase state + rebase_merge = os.path.join(unpack_path, '.git', 'rebase-merge') + rebase_apply = os.path.join(unpack_path, '.git', 'rebase-apply') + self.assertFalse(os.path.exists(rebase_merge), + "rebase-merge dir should not exist after failed unpack_update") + self.assertFalse(os.path.exists(rebase_apply), + "rebase-apply dir should not exist after failed unpack_update") + + def test_unpack_update_untracked_file_overwritten_by_upstream(self): + """Test that unpack_update raises UnpackError when an untracked file would be + overwritten by an incoming upstream commit. + + We skip untracked files in the pre-check (git rebase doesn't touch harmless + untracked files), but git itself refuses to rebase when an untracked file would + be overwritten by the incoming changes. The resulting FetchError must be caught + and re-raised as UnpackError without leaving the repo in a mid-rebase state. + + Two sub-cases are covered: + - top-level untracked file clashing with an incoming upstream file + - untracked file inside a subdirectory (xxx/somefile) clashing with an + upstream commit that adds the same path + """ + def _run_case(upstream_path, local_rel_path, commit_msg): + """ + Add upstream_path to self.srcdir, create local_rel_path as an + untracked file in the checkout, then assert that unpack_update + raises UnpackError and leaves no mid-rebase state, and that the + local file is untouched. + """ + # Fresh fetch + unpack at the current SRCREV + fetcher = bb.fetch2.Fetch([self.d.getVar('SRC_URI')], self.d) + fetcher.download() + fetcher.unpack(self.unpackdir) + + unpack_path = os.path.join(self.unpackdir, 'git') + + # Upstream adds the file (potentially inside a subdirectory) + full_upstream = os.path.join(self.srcdir, upstream_path) + os.makedirs(os.path.dirname(full_upstream), exist_ok=True) + with open(full_upstream, 'w') as f: + f.write('upstream content\n') + self.git(['add', upstream_path], cwd=self.srcdir) + self.git(['commit', '-m', commit_msg], cwd=self.srcdir) + new_rev = self.git(['rev-parse', 'HEAD'], cwd=self.srcdir).strip() + + # Create the clashing untracked file in the checkout + full_local = os.path.join(unpack_path, local_rel_path) + os.makedirs(os.path.dirname(full_local), exist_ok=True) + with open(full_local, 'w') as f: + f.write('local untracked content\n') + + self.d.setVar('SRCREV', new_rev) + fetcher = bb.fetch2.Fetch([self.d.getVar('SRC_URI')], self.d) + fetcher.download() + + uri = self.d.getVar('SRC_URI') + ud = fetcher.ud[uri] + git_fetcher = ud.method + + # git rebase refuses because the untracked file would be overwritten + with self.assertRaises(bb.fetch2.UnpackError): + git_fetcher.unpack_update(ud, self.unpackdir, self.d) + + # Repo must not be left in a mid-rebase state + self.assertFalse(os.path.exists(os.path.join(unpack_path, '.git', 'rebase-merge'))) + self.assertFalse(os.path.exists(os.path.join(unpack_path, '.git', 'rebase-apply'))) + + # The local untracked file must be untouched + self.assertTrue(os.path.exists(full_local)) + with open(full_local) as f: + self.assertEqual(f.read(), 'local untracked content\n') + + # Reset unpackdir for the next sub-case + import shutil as _shutil + _shutil.rmtree(self.unpackdir) + os.makedirs(self.unpackdir) + + # Sub-case 1: top-level file clash + _run_case('newfile.txt', 'newfile.txt', + 'Upstream adds newfile.txt') + + # Sub-case 2: file inside a subdirectory (xxx/somefile) + _run_case('xxx/somefile.txt', 'xxx/somefile.txt', + 'Upstream adds xxx/somefile.txt') + + def test_unpack_update_shallow_clone_fails(self): + """Test that unpack_update raises UnpackError for shallow-tarball checkouts. + + Shallow clones lack full history, which makes an in-place rebase impossible + without network access. After fetching with BB_GIT_SHALLOW=1 the clonedir + is deleted so that unpack() is forced to use the shallow tarball. + A subsequent call to unpack_update() must raise UnpackError and the message + must mention 'shallow clone' so callers can distinguish this case. + """ + self.d.setVar('BB_GIT_SHALLOW', '1') + self.d.setVar('BB_GENERATE_SHALLOW_TARBALLS', '1') + + # First fetch at initial revision + fetcher = bb.fetch2.Fetch([self.d.getVar('SRC_URI')], self.d) + fetcher.download() + + # Remove clonedir to force use of shallow tarball + clonedir = os.path.join(self.dldir, 'git2') + if os.path.exists(clonedir): + shutil.rmtree(clonedir) + + fetcher.unpack(self.unpackdir) + + # Update to new revision + self.d.setVar('SRCREV', self.updated_rev) + fetcher = bb.fetch2.Fetch([self.d.getVar('SRC_URI')], self.d) + fetcher.download() + + # unpack_update should fail for shallow clones + uri = self.d.getVar('SRC_URI') + ud = fetcher.ud[uri] + git_fetcher = ud.method + + with self.assertRaises(bb.fetch2.UnpackError) as context: + git_fetcher.unpack_update(ud, self.unpackdir, self.d) + + self.assertIn("shallow clone", str(context.exception).lower()) + + def test_unpack_update_stale_dldir_remote(self): + """Test that unpack_update raises UnpackError when the dldir remote URL is stale. + + If the clonedir has been removed after the initial unpack (e.g. DL_DIR was + cleaned) the 'dldir' remote URL no longer resolves. The fetch inside + update_mode will fail with a FetchError which must be re-raised as + UnpackError so the caller can fall back to a full re-fetch. + """ + fetcher = bb.fetch2.Fetch([self.d.getVar('SRC_URI')], self.d) + fetcher.download() + fetcher.unpack(self.unpackdir) + + unpack_path = os.path.join(self.unpackdir, 'git') + + # Advance SRCREV to trigger update_mode + self.d.setVar('SRCREV', self.updated_rev) + fetcher = bb.fetch2.Fetch([self.d.getVar('SRC_URI')], self.d) + fetcher.download() + + uri = self.d.getVar('SRC_URI') + ud = fetcher.ud[uri] + + # Delete the clonedir and corrupt the dldir remote URL so that + # 'git fetch dldir' fails, simulating a missing or relocated DL_DIR. + shutil.rmtree(ud.clonedir) + self.git(['remote', 'set-url', 'dldir', 'file://' + ud.clonedir], + cwd=unpack_path) + + git_fetcher = ud.method + with self.assertRaises(bb.fetch2.UnpackError): + git_fetcher.unpack_update(ud, self.unpackdir, self.d) + + def test_fetch_unpack_update_toplevel_api(self): + """Test that the top-level Fetch.unpack_update() dispatches to Git.unpack_update(). + + Callers such as bitbake-setup use fetcher.unpack_update(root) rather than + calling the method on the Git fetcher directly. Verify that the public API + works end-to-end: fetch at initial_rev, unpack, advance to updated_rev, + fetch again, then call fetcher.unpack_update(root) and confirm the content + is updated. + """ + fetcher = bb.fetch2.Fetch([self.d.getVar('SRC_URI')], self.d) + fetcher.download() + fetcher.unpack(self.unpackdir) + + unpack_path = os.path.join(self.unpackdir, 'git') + with open(os.path.join(unpack_path, 'testfile.txt')) as f: + self.assertEqual(f.read(), 'initial content\n') + + self.d.setVar('SRCREV', self.updated_rev) + fetcher = bb.fetch2.Fetch([self.d.getVar('SRC_URI')], self.d) + fetcher.download() + + # Use the public Fetch.unpack_update() rather than the method directly + fetcher.unpack_update(self.unpackdir) + + with open(os.path.join(unpack_path, 'testfile.txt')) as f: + self.assertEqual(f.read(), 'updated content\n') From patchwork Wed Mar 25 06:51:45 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: AdrianF X-Patchwork-Id: 84315 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 7DF8EFEA828 for ; Wed, 25 Mar 2026 07:14:12 +0000 (UTC) Received: from mta-64-227.siemens.flowmailer.net (mta-64-227.siemens.flowmailer.net [185.136.64.227]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.17083.1774422847652154619 for ; Wed, 25 Mar 2026 00:14:09 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=adrian.freihofer@siemens.com header.s=fm1 header.b=RcPYbYmN; spf=pass (domain: rts-flowmailer.siemens.com, ip: 185.136.64.227, mailfrom: fm-1329275-20260325071404aeeb10ca7b0002071f-omcnjg@rts-flowmailer.siemens.com) Received: by mta-64-227.siemens.flowmailer.net with ESMTPSA id 20260325071404aeeb10ca7b0002071f for ; Wed, 25 Mar 2026 08:14:05 +0100 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; s=fm1; d=siemens.com; i=adrian.freihofer@siemens.com; h=Date:From:Subject:To:Message-ID:MIME-Version:Content-Type:Content-Transfer-Encoding:Cc:References:In-Reply-To; bh=lppszxLz4QTVAYbLIpXDV4/5Xlh/vWpcjTQZo9Ctb+0=; b=RcPYbYmNNoEamp03VNZ+B5HwGp29cE3BEnYGf3MjhVMZuqwvtNQuJ1z9Z8Aeo/4A1vix9B Z025GDfQw/HDXsyPl90Jgc/a7Q6Uoc6OMFZI8R/6e+7njIn7VGF/CpBaLmeYvb0ku6XnB2hX xai9A30GwFm+yOVOUm66P4EIMFwzRPhjUxC2SHD9smbhl0JSMT9GW5MBysK+G38J5brjkvl3 fcD8WLFUAq7WjvWc/FA5mKL9rUeFF81G4HL8+IzxRcICq2WaaiKw7Hy2LcLppxa1RW6Ae98A owTPWKtZcbfzZvkwqf1J2SeWdHMd6pQLOT8Boq6pE4TDiolPOXOvdqjQ==; From: AdrianF To: bitbake-devel@lists.openembedded.org Cc: Adrian Freihofer Subject: [PATCH v2 03/10] tests/fetch: remove unused import, fix trailing whitespace Date: Wed, 25 Mar 2026 07:51:45 +0100 Message-ID: <20260325071342.47272-4-adrian.freihofer@siemens.com> In-Reply-To: <20260325071342.47272-1-adrian.freihofer@siemens.com> References: <20260325071342.47272-1-adrian.freihofer@siemens.com> MIME-Version: 1.0 X-Flowmailer-Platform: Siemens Feedback-ID: 519:519-1329275:519-21489:flowmailer List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Wed, 25 Mar 2026 07:14:12 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/bitbake-devel/message/19226 From: Adrian Freihofer Just cleanup, no functional change. Signed-off-by: Adrian Freihofer --- lib/bb/tests/fetch.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/bb/tests/fetch.py b/lib/bb/tests/fetch.py index 25432e030..570865106 100644 --- a/lib/bb/tests/fetch.py +++ b/lib/bb/tests/fetch.py @@ -18,7 +18,6 @@ import os import signal import tarfile from bb.fetch2 import URI -from bb.fetch2 import FetchMethod import bb import bb.utils from bb.tests.support.httpserver import HTTPService @@ -551,8 +550,8 @@ class MirrorUriTest(FetcherTest): fetcher = bb.fetch.FetchData("http://downloads.yoctoproject.org/releases/bitbake/bitbake-1.0.tar.gz", self.d) mirrors = bb.fetch2.mirror_from_string(mirrorvar) uris, uds = bb.fetch2.build_mirroruris(fetcher, mirrors, self.d) - self.assertEqual(uris, ['file:///somepath/downloads/bitbake-1.0.tar.gz', - 'file:///someotherpath/downloads/bitbake-1.0.tar.gz', + self.assertEqual(uris, ['file:///somepath/downloads/bitbake-1.0.tar.gz', + 'file:///someotherpath/downloads/bitbake-1.0.tar.gz', 'http://otherdownloads.yoctoproject.org/downloads/bitbake-1.0.tar.gz', 'http://downloads2.yoctoproject.org/downloads/bitbake-1.0.tar.gz']) @@ -1390,7 +1389,7 @@ class URLHandle(unittest.TestCase): "https://somesite.com/somerepo.git;user=anyUser:idtoken=1234" : ('https', 'somesite.com', '/somerepo.git', '', '', {'user': 'anyUser:idtoken=1234'}), 'git://s.o-me_ONE:%s@git.openembedded.org/bitbake;branch=main;protocol=https' % password: ('git', 'git.openembedded.org', '/bitbake', 's.o-me_ONE', password, {'branch': 'main', 'protocol' : 'https'}), } - # we require a pathname to encodeurl but users can still pass such urls to + # we require a pathname to encodeurl but users can still pass such urls to # decodeurl and we need to handle them decodedata = datatable.copy() decodedata.update({ From patchwork Wed Mar 25 06:51:46 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: AdrianF X-Patchwork-Id: 84314 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 62906FEA824 for ; Wed, 25 Mar 2026 07:14:12 +0000 (UTC) Received: from mta-65-225.siemens.flowmailer.net (mta-65-225.siemens.flowmailer.net [185.136.65.225]) by mx.groups.io with SMTP id smtpd.msgproc01-g2.16761.1774422847649927255 for ; Wed, 25 Mar 2026 00:14:08 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=adrian.freihofer@siemens.com header.s=fm1 header.b=CLiPZyga; spf=pass (domain: rts-flowmailer.siemens.com, ip: 185.136.65.225, mailfrom: fm-1329275-202603250714051f1e89d3250002071e-oihlzf@rts-flowmailer.siemens.com) Received: by mta-65-225.siemens.flowmailer.net with ESMTPSA id 202603250714051f1e89d3250002071e for ; Wed, 25 Mar 2026 08:14:05 +0100 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; s=fm1; d=siemens.com; i=adrian.freihofer@siemens.com; h=Date:From:Subject:To:Message-ID:MIME-Version:Content-Type:Content-Transfer-Encoding:Cc:References:In-Reply-To; bh=lQl+SDuMsEQlaWthQityfwqt4cL3OxMVKG3k/JLkdZ4=; b=CLiPZyga9ZiU6VsClpmlXg6HcK9VFlpAgw2IQzV470TkEGuGjhk5i8yhUP7M7F/jLj4LMz bgg+sRVr2c9Ymxz85Gfo2zfO+RIzRrWK7jAGaA8nXWkuGSmX0961WahTrQz8jk9wYqPNG/kg gwn4djl5tHC/u5kyksiLY/Va30g8jsSFQKVmjntlq3u1szpTYLhTMDjJdFts6QfLOEB59XMj oVJ3NoNpY6NLRm+tu9WC9D38kio8Rzq1ti+rRstV7o4K8+6uQ/M0F7crPEiQoMxsgAvTfSbU qFDzlMOmfcjnPEQMQpMVQDhZGS8X8nz1ennKqna/qTQaVwqcI7UTxBpw==; From: AdrianF To: bitbake-devel@lists.openembedded.org Cc: Adrian Freihofer Subject: [PATCH v2 04/10] bitbake-setup: always restore sys.stdout Date: Wed, 25 Mar 2026 07:51:46 +0100 Message-ID: <20260325071342.47272-5-adrian.freihofer@siemens.com> In-Reply-To: <20260325071342.47272-1-adrian.freihofer@siemens.com> References: <20260325071342.47272-1-adrian.freihofer@siemens.com> MIME-Version: 1.0 X-Flowmailer-Platform: Siemens Feedback-ID: 519:519-1329275:519-21489:flowmailer List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Wed, 25 Mar 2026 07:14:12 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/bitbake-devel/message/19224 From: Adrian Freihofer While working on the bitbake-setup update with a non destructive fetcher, I noticed that if the fetcher raises an exception, sys.stdout is not restored, which can lead to issues in the rest of the code. This change ensures that sys.stdout is always restored, even if an exception occurs during the fetch and unpack process. Showing the full Traceback of the error would be a bit confusing because it happened in code which is not yet ready for review, but the final error was: ValueError: I/O operation on closed file. and this little change seems to be a reasonable improvement to avoid such issues. Signed-off-by: Adrian Freihofer --- bin/bitbake-setup | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/bin/bitbake-setup b/bin/bitbake-setup index dcad9c169..e8d520687 100755 --- a/bin/bitbake-setup +++ b/bin/bitbake-setup @@ -793,9 +793,11 @@ def do_fetch(fetcher, dir): with open(fetchlog, 'a') as f: oldstdout = sys.stdout sys.stdout = f - fetcher.download() - fetcher.unpack_update(dir) - sys.stdout = oldstdout + try: + fetcher.download() + fetcher.unpack_update(dir) + finally: + sys.stdout = oldstdout def update_registry(registry, cachedir, d): registrydir = 'configurations' From patchwork Wed Mar 25 06:51:47 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: AdrianF X-Patchwork-Id: 84312 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 96307FEA81E for ; Wed, 25 Mar 2026 07:14:11 +0000 (UTC) Received: from mta-64-225.siemens.flowmailer.net (mta-64-225.siemens.flowmailer.net [185.136.64.225]) by mx.groups.io with SMTP id smtpd.msgproc01-g2.16764.1774422847650445527 for ; Wed, 25 Mar 2026 00:14:08 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=adrian.freihofer@siemens.com header.s=fm1 header.b=bB5s/BDL; spf=pass (domain: rts-flowmailer.siemens.com, ip: 185.136.64.225, mailfrom: fm-1329275-20260325071405e1f914f30300020761-aqyr0e@rts-flowmailer.siemens.com) Received: by mta-64-225.siemens.flowmailer.net with ESMTPSA id 20260325071405e1f914f30300020761 for ; Wed, 25 Mar 2026 08:14:05 +0100 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; s=fm1; d=siemens.com; i=adrian.freihofer@siemens.com; h=Date:From:Subject:To:Message-ID:MIME-Version:Content-Type:Content-Transfer-Encoding:Cc:References:In-Reply-To; bh=ZvsrtByuHhRGhYvFKB/jsVqT3GPrbDYzgxb3raFvR1M=; b=bB5s/BDLIC94RjeLsf3pUAO/e9/3Pg7Yau5E8XqM5K5DI8fRYsl39rNWk7IOerX6tPjljr rEjqHeYmuSk6S8bbKixeXBzWV5+pH5zEsrq/JBVhXHzKTBZSh4Rzb0Nk2BwD9p13NWD787sr RlQ6S5OSprvXlLiSocrTwd9fDGSetgLSGpPQMWdjKaX+Tz7v/MiCtflPPAR9k9v9PEVgIsWC DPYHaksdu7rNrDFJ32XJCbmSD1jCKCmJQQYhSAdUE4Wl8HhIAQvD+Crws0h/HED5T7YAlwh2 tESJ72h5Bs9uqpHteihtbPHn0WuxVo4HuGE+r1Q2TCQF1MkfduN7vV/Q==; From: AdrianF To: bitbake-devel@lists.openembedded.org Cc: Adrian Freihofer Subject: [PATCH v2 05/10] tests/setup: cleanup imports Date: Wed, 25 Mar 2026 07:51:47 +0100 Message-ID: <20260325071342.47272-6-adrian.freihofer@siemens.com> In-Reply-To: <20260325071342.47272-1-adrian.freihofer@siemens.com> References: <20260325071342.47272-1-adrian.freihofer@siemens.com> MIME-Version: 1.0 X-Flowmailer-Platform: Siemens Feedback-ID: 519:519-1329275:519-21489:flowmailer List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Wed, 25 Mar 2026 07:14:11 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/bitbake-devel/message/19221 From: Adrian Freihofer Add missing top-level imports for os, stat, bb and bb.process and remove the redundant inline 'import os' and 'import stat' that were inside individual methods. Signed-off-by: Adrian Freihofer --- lib/bb/tests/setup.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/bb/tests/setup.py b/lib/bb/tests/setup.py index a66f05b36..d52a81395 100644 --- a/lib/bb/tests/setup.py +++ b/lib/bb/tests/setup.py @@ -5,9 +5,13 @@ # from bb.tests.fetch import FetcherTest -import json -import hashlib +import bb +import bb.process import glob +import hashlib +import json +import os +import stat from bb.tests.support.httpserver import HTTPService class BitbakeSetupTest(FetcherTest): @@ -208,7 +212,6 @@ print("BBPATH is {{}}".format(os.environ["BBPATH"])) with open(fullname, 'w') as f: f.write(content) if script: - import stat st = os.stat(fullname) os.chmod(fullname, st.st_mode | stat.S_IEXEC) self.git('add {}'.format(name), cwd=self.testrepopath) @@ -279,7 +282,6 @@ print("BBPATH is {{}}".format(os.environ["BBPATH"])) def test_setup(self): # unset BBPATH to ensure tests run in isolation from the existing bitbake environment - import os if 'BBPATH' in os.environ: del os.environ['BBPATH'] From patchwork Wed Mar 25 06:51:48 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: AdrianF X-Patchwork-Id: 84317 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id C5639FEA82E for ; Wed, 25 Mar 2026 07:14:12 +0000 (UTC) Received: from mta-65-226.siemens.flowmailer.net (mta-65-226.siemens.flowmailer.net [185.136.65.226]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.17085.1774422847652635894 for ; Wed, 25 Mar 2026 00:14:09 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=adrian.freihofer@siemens.com header.s=fm1 header.b=EWbKDBtW; spf=pass (domain: rts-flowmailer.siemens.com, ip: 185.136.65.226, mailfrom: fm-1329275-2026032507140509b76e9a230002077d-txhmhy@rts-flowmailer.siemens.com) Received: by mta-65-226.siemens.flowmailer.net with ESMTPSA id 2026032507140509b76e9a230002077d for ; Wed, 25 Mar 2026 08:14:05 +0100 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; s=fm1; d=siemens.com; i=adrian.freihofer@siemens.com; h=Date:From:Subject:To:Message-ID:MIME-Version:Content-Type:Content-Transfer-Encoding:Cc:References:In-Reply-To; bh=tHatzJkj8v41S00Pvw0rYn2RiqmtE0gHkGSd5U3RR+E=; b=EWbKDBtWruBT++t7xA0P8v4LvSqCl+SQblh47YSWDetz1qbWYqsvLieYUr3SUo9c/VN60T frab2UcNfup37y4zPCVSmzKdt5/RlBxQpWgAKS7ZV39V4Ij3RM6EwBGzXTrRInyIrlnRgveu qozYh54ye8ihywfMpNC/Qxw90Rj5on8+6cDx/8E8Fj6ybuOCXYd6ZWyFKC+6OVG/hgU8GfGy z1RjrrO7PYfXHLQ8b3CeO8xV3xFSe6z+iS4URXmAqPTm512r2JoCFhkW7Ja7lBDB6IcpuhDu IKDqoa1K/6t8cyhCgYTymZ11JYYWB67099FsOSPNqVJwG9J6u5Yi4afA==; From: AdrianF To: bitbake-devel@lists.openembedded.org Cc: Adrian Freihofer Subject: [PATCH v2 06/10] tests/setup: fix dead check_setupdir_files guards Date: Wed, 25 Mar 2026 07:51:48 +0100 Message-ID: <20260325071342.47272-7-adrian.freihofer@siemens.com> In-Reply-To: <20260325071342.47272-1-adrian.freihofer@siemens.com> References: <20260325071342.47272-1-adrian.freihofer@siemens.com> MIME-Version: 1.0 X-Flowmailer-Platform: Siemens Feedback-ID: 519:519-1329275:519-21489:flowmailer List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Wed, 25 Mar 2026 07:14:12 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/bitbake-devel/message/19229 From: Adrian Freihofer Two conditions in check_setupdir_files used wrong key names and so never triggered: - 'oe-fragment' -> 'oe-fragments' (plural): fragment-existence assertions were never reached for any variant that has fragments. - 'bb-environment-passthrough' -> 'bb-env-passthrough-additions': BB_ENV_PASSTHROUGH_ADDITIONS assertions were never reached for the gizmo-env-passthrough variant. Also drop the redundant .keys() call on both guards. Signed-off-by: Adrian Freihofer --- lib/bb/tests/setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/bb/tests/setup.py b/lib/bb/tests/setup.py index d52a81395..1af3d8b50 100644 --- a/lib/bb/tests/setup.py +++ b/lib/bb/tests/setup.py @@ -260,11 +260,11 @@ print("BBPATH is {{}}".format(os.environ["BBPATH"])) ) self.assertIn(filerelative_layer, bblayers) - if 'oe-fragment' in bitbake_config.keys(): + if 'oe-fragments' in bitbake_config: for f in bitbake_config["oe-fragments"]: self.assertTrue(os.path.exists(os.path.join(bb_conf_path, f))) - if 'bb-environment-passthrough' in bitbake_config.keys(): + if 'bb-env-passthrough-additions' in bitbake_config: with open(os.path.join(bb_build_path, 'init-build-env'), 'r') as f: init_build_env = f.read() self.assertTrue('BB_ENV_PASSTHROUGH_ADDITIONS' in init_build_env) From patchwork Wed Mar 25 06:51:49 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: AdrianF X-Patchwork-Id: 84311 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 2EF29FEA81C for ; Wed, 25 Mar 2026 07:14:11 +0000 (UTC) Received: from mta-64-228.siemens.flowmailer.net (mta-64-228.siemens.flowmailer.net [185.136.64.228]) by mx.groups.io with SMTP id smtpd.msgproc01-g2.16765.1774422847650728674 for ; Wed, 25 Mar 2026 00:14:08 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=adrian.freihofer@siemens.com header.s=fm1 header.b=RoOh1FoR; spf=pass (domain: rts-flowmailer.siemens.com, ip: 185.136.64.228, mailfrom: fm-1329275-20260325071405107f536f5b00020761-n8_y0t@rts-flowmailer.siemens.com) Received: by mta-64-228.siemens.flowmailer.net with ESMTPSA id 20260325071405107f536f5b00020761 for ; Wed, 25 Mar 2026 08:14:05 +0100 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; s=fm1; d=siemens.com; i=adrian.freihofer@siemens.com; h=Date:From:Subject:To:Message-ID:MIME-Version:Content-Type:Content-Transfer-Encoding:Cc:References:In-Reply-To; bh=owFlcwP31Gm5TeqHR7w0fX9n7gq24jHO/hgERsrv6+Y=; b=RoOh1FoRamIjAMOqZizQccMPqOylmMNIyPdv8I2NAIX/4qh56oODYn3LECyMYjKvIXpkfv XFAjK0Lr3BlykJ3rSlpDzeBVsoUQZ4cTv5B1rbqpVF2IKhRRAODXfdRblJeLuiIXtKZc4Tje HYpf06bnF9XDcZc0FKWadIQFZt7sh63MOF3Te4G9HMGf4G5y7h29HaZ/8hz9J07rbxjsA7CE js9R46dq/+o3uzZqqcOrWelVA2CmMIySTwbr9zIxuEMTxxFft+QjKsSgbodZJE7XfoM82qhB N+PM600hr4B5wiRgsJcaw1cFfdZTLEqA26Hew/ASoBmtjkaIA5+8qFNw==; From: AdrianF To: bitbake-devel@lists.openembedded.org Cc: Adrian Freihofer Subject: [PATCH v2 07/10] bitbake-setup: generate config files for VSCode Date: Wed, 25 Mar 2026 07:51:49 +0100 Message-ID: <20260325071342.47272-8-adrian.freihofer@siemens.com> In-Reply-To: <20260325071342.47272-1-adrian.freihofer@siemens.com> References: <20260325071342.47272-1-adrian.freihofer@siemens.com> MIME-Version: 1.0 X-Flowmailer-Platform: Siemens Feedback-ID: 519:519-1329275:519-21489:flowmailer List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Wed, 25 Mar 2026 07:14:11 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/bitbake-devel/message/19220 From: Adrian Freihofer This change introduces a function to generate a VSCode workspace file (bitbake.code-workspace). The --init-vscode flag added to bitbake-setup init defaults to True when the code binary is found on PATH, and can be passed explicitly to exercise the feature on machines without code (e.g. when running tests on an autobuilder). Once the workspace file exists, it is updated automatically on every subsequent run. This workspace file is preferred over a project-specific .vscode/settings.json for several reasons: - It allows for a multi-root workspace, which is ideal for a bitbake project structure setup with bitbake-setup. This enables including all layer repositories and the build configuration directory as top-level folders in the explorer. - The workspace file can be located at the top level of the setup, outside of any version-controlled source directory. This avoids cluttering the git repositories with editor-specific configuration. - It provides a centralized place for all VSCode settings related to the project, including those for the bitbake extension, Python language server, and file associations, ensuring a consistent development environment for all users of the project. The Python analysis paths (python.analysis.extraPaths) are configured with absolute paths. This is a workaround for a limitation in the Pylance extension, which does not correctly resolve ${workspaceFolder:...} variables in a multi-root workspace context for import resolution. Using absolute paths ensures that Pylance can find all necessary modules from the various layers. There is room for improvement. Starting a terminal (bitbake or any other) is cumbersome, as VSCode wants to start it for one of the layers rather than the build directory. Signed-off-by: Adrian Freihofer --- bin/bitbake-setup | 175 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 168 insertions(+), 7 deletions(-) diff --git a/bin/bitbake-setup b/bin/bitbake-setup index e8d520687..bebf2e705 100755 --- a/bin/bitbake-setup +++ b/bin/bitbake-setup @@ -239,7 +239,7 @@ bitbake-setup init -L {} /path/to/repo/checkout""".format( return layers_fixed_revisions -def setup_bitbake_build(bitbake_config, layerdir, setupdir, thisdir, update_bb_conf): +def setup_bitbake_build(bitbake_config, layerdir, setupdir, thisdir, update_bb_conf, init_vscode=False): def _setup_build_conf(layers, filerelative_layers, build_conf_dir): os.makedirs(build_conf_dir) layers_s = [] @@ -358,6 +358,7 @@ def setup_bitbake_build(bitbake_config, layerdir, setupdir, thisdir, update_bb_c init_script = os.path.join(bitbake_builddir, "init-build-env") + workspace_file = os.path.join(setupdir, "bitbake.code-workspace") shell = "bash" fragments = bitbake_config.get("oe-fragments", []) + sorted(bitbake_config.get("oe-fragment-choices",{}).values()) if fragments: @@ -369,6 +370,8 @@ def setup_bitbake_build(bitbake_config, layerdir, setupdir, thisdir, update_bb_c logger.plain('New bitbake configuration from upstream is the same as the current one, no need to update it.') shutil.rmtree(bitbake_confdir) os.rename(backup_bitbake_confdir, bitbake_confdir) + if init_vscode or os.path.exists(workspace_file): + configure_vscode(setupdir, layerdir, bitbake_builddir, init_script) return logger.plain('Upstream bitbake configuration changes were found:') @@ -384,6 +387,8 @@ def setup_bitbake_build(bitbake_config, layerdir, setupdir, thisdir, update_bb_c logger.plain(f'Leaving the upstream configuration in {upstream_bitbake_confdir}') os.rename(bitbake_confdir, upstream_bitbake_confdir) os.rename(backup_bitbake_confdir, bitbake_confdir) + if init_vscode or os.path.exists(workspace_file): + configure_vscode(setupdir, layerdir, bitbake_builddir, init_script) return logger.plain('Applying upstream bitbake configuration changes') @@ -391,17 +396,21 @@ def setup_bitbake_build(bitbake_config, layerdir, setupdir, thisdir, update_bb_c fragment_note = "Run 'bitbake-config-build enable-fragment ' to enable additional fragments or replace built-in ones (e.g. machine/ or distro/ to change MACHINE or DISTRO)." + readme_extra = "" + if init_vscode: + readme_extra = "\n\nTo edit the code in VSCode, open the workspace: code {}\n".format(workspace_file) + readme = """{}\n\nAdditional information is in {} and {}\n Source the environment using '. {}' to run builds from the command line.\n {}\n -The bitbake configuration files (local.conf, bblayers.conf and more) can be found in {}/conf -""".format( +The bitbake configuration files (local.conf, bblayers.conf and more) can be found in {}/conf{}""".format( bitbake_config["description"], os.path.join(bitbake_builddir,'conf/conf-summary.txt'), os.path.join(bitbake_builddir,'conf/conf-notes.txt'), init_script, fragment_note, - bitbake_builddir + bitbake_builddir, + readme_extra ) readme_file = os.path.join(bitbake_builddir, "README") with open(readme_file, 'w') as f: @@ -412,6 +421,11 @@ The bitbake configuration files (local.conf, bblayers.conf and more) can be foun logger.plain("To run builds, source the environment using\n . {}\n".format(init_script)) logger.plain("{}\n".format(fragment_note)) logger.plain("The bitbake configuration files (local.conf, bblayers.conf and more) can be found in\n {}/conf\n".format(bitbake_builddir)) + if init_vscode: + logger.plain("To edit the code in VSCode, open the workspace:\n code {}\n".format(workspace_file)) + + if init_vscode or os.path.exists(workspace_file): + configure_vscode(setupdir, layerdir, bitbake_builddir, init_script) def get_registry_config(registry_path, id): for root, dirs, files in os.walk(registry_path): @@ -427,12 +441,12 @@ def merge_overrides_into_sources(sources, overrides): layers[k] = v return layers -def update_build(config, confdir, setupdir, layerdir, d, update_bb_conf="prompt"): +def update_build(config, confdir, setupdir, layerdir, d, update_bb_conf="prompt", init_vscode=False): layer_config = merge_overrides_into_sources(config["data"]["sources"], config["source-overrides"]["sources"]) sources_fixed_revisions = checkout_layers(layer_config, confdir, layerdir, d) bitbake_config = config["bitbake-config"] thisdir = os.path.dirname(config["path"]) if config["type"] == 'local' else None - setup_bitbake_build(bitbake_config, layerdir, setupdir, thisdir, update_bb_conf) + setup_bitbake_build(bitbake_config, layerdir, setupdir, thisdir, update_bb_conf, init_vscode) write_sources_fixed_revisions(confdir, layerdir, sources_fixed_revisions) commit_config(confdir) @@ -621,6 +635,151 @@ def obtain_overrides(args): return overrides +def configure_vscode(setupdir, layerdir, builddir, init_script): + """ + Configure the VSCode environment by creating or updating a workspace file. + + Create or update a bitbake.code-workspace file with folders for the layers and build/conf. + Managed folders are regenerated; user-added folders are kept. Settings are merged, with + managed keys (bitbake.*, python extra paths) always overwritten. + """ + logger.debug("configure_vscode: setupdir={}, layerdir={}, builddir={}, init_script={}".format( + setupdir, layerdir, builddir, init_script)) + + # Get git repository directories + git_repos = [] + if os.path.exists(layerdir): + for entry in os.listdir(layerdir): + entry_path = os.path.join(layerdir, entry) + if os.path.isdir(entry_path) and not os.path.islink(entry_path): + # Check if it's a git repository + if os.path.exists(os.path.join(entry_path, '.git')): + git_repos.append(entry) + logger.debug("configure_vscode: found {} git repos: {}".format(len(git_repos), git_repos)) + + conf_path = os.path.relpath(os.path.join(builddir, "conf"), setupdir) + repo_paths = [os.path.relpath(os.path.join(layerdir, repo), setupdir) for repo in git_repos] + logger.debug("configure_vscode: conf_path={}, repo_paths={}".format(conf_path, repo_paths)) + + # Load existing workspace + workspace_file = os.path.join(setupdir, "bitbake.code-workspace") + workspace = { + "extensions": { + "recommendations": [ + "yocto-project.yocto-bitbake" + ] + } + } + if os.path.exists(workspace_file): + logger.debug("configure_vscode: loading existing workspace file: {}".format(workspace_file)) + try: + with open(workspace_file, 'r') as f: + workspace = json.load(f) + logger.debug("configure_vscode: loaded workspace with {} folders, {} settings".format( + len(workspace.get("folders", [])), len(workspace.get("settings", {})))) + except (json.JSONDecodeError, OSError) as e: + logger.error( + "Unable to read existing workspace file {}: {}. Skipping update.".format( + workspace_file, str(e) + ) + ) + return + else: + logger.debug("configure_vscode: creating new workspace file: {}".format(workspace_file)) + + # Update folders + existing_folders = workspace.get("folders", []) + new_folders = [{"name": "conf", "path": conf_path}] + for rp in repo_paths: + repo_name = os.path.basename(rp) + new_folders.append({"name": repo_name, "path": rp}) + # Keep any user-added folders that are not managed + managed_paths = {f["path"] for f in new_folders} + for f in existing_folders: + if f["path"] not in managed_paths: + new_folders.append(f) + logger.debug("configure_vscode: keeping user-added folder: {}".format(f["path"])) + workspace["folders"] = new_folders + logger.debug("configure_vscode: updated workspace with {} folders".format(len(new_folders))) + + # Build Python extra paths for each layer - only check top level of each repo + extra_paths = [] + subdirs_to_check = ['lib', 'scripts'] + for repo in git_repos: + repo_path_abs = os.path.join(layerdir, repo) + for subdir in subdirs_to_check: + sub_path = os.path.join(repo_path_abs, subdir) + if os.path.isdir(sub_path): + extra_paths.append(sub_path) + + # Update settings + existing_settings = workspace.get("settings", {}) + new_settings = { + "bitbake.disableConfigModification": True, + "bitbake.pathToBitbakeFolder": os.path.join(layerdir, "bitbake"), + "bitbake.pathToBuildFolder": builddir, + "bitbake.pathToEnvScript": init_script, + "bitbake.workingDirectory": builddir, + "files.associations": { + "*.conf": "bitbake", + "*.inc": "bitbake" + }, + "files.exclude": { + "**/.git/**": True + }, + "search.exclude": { + "**/.git/**": True, + "**/logs/**": True + }, + "files.watcherExclude": { + "**/.git/**": True, + "**/logs/**": True + }, + "python.analysis.exclude": [ + "**/.git/**", + "**/logs/**" + ], + "python.autoComplete.extraPaths": extra_paths, + "python.analysis.extraPaths": extra_paths + } + + # Merge settings: add missing, always update bitbake paths and python extra paths + for key, value in new_settings.items(): + if key not in existing_settings: + existing_settings[key] = value + elif key.startswith("bitbake.") or key in [ + "python.autoComplete.extraPaths", + "python.analysis.extraPaths", + ]: + # Always replace - these are managed/machine-generated settings + existing_settings[key] = value + elif key in [ + "files.associations", + "files.exclude", + "search.exclude", + "files.watcherExclude", + "python.analysis.exclude", + ]: + # For dicts and lists, merge new values in without removing user additions + if isinstance(value, dict): + if not isinstance(existing_settings[key], dict): + existing_settings[key] = {} + for k, v in value.items(): + if k not in existing_settings[key]: + existing_settings[key][k] = v + elif isinstance(value, list): + if not isinstance(existing_settings[key], list): + existing_settings[key] = [] + for item in value: + if item not in existing_settings[key]: + existing_settings[key].append(item) + + workspace["settings"] = existing_settings + logger.debug("configure_vscode: merged settings, total {} keys".format(len(existing_settings))) + + with open(workspace_file, 'w') as f: + json.dump(workspace, f, indent=4) + logger.debug("configure_vscode: wrote workspace file: {}".format(workspace_file)) def init_config(top_dir, settings, args): create_siteconf(top_dir, args.non_interactive, settings) @@ -690,7 +849,7 @@ def init_config(top_dir, settings, args): bb.event.register("bb.build.TaskProgress", handle_task_progress, data=d) write_upstream_config(confdir, upstream_config) - update_build(upstream_config, confdir, setupdir, layerdir, d, update_bb_conf="yes") + update_build(upstream_config, confdir, setupdir, layerdir, d, update_bb_conf="yes", init_vscode=args.init_vscode) bb.event.remove("bb.build.TaskProgress", None) @@ -1086,6 +1245,8 @@ def main(): parser_init.add_argument('--skip-selection', action='append', help='Do not select and set an option/fragment from available choices; the resulting bitbake configuration may be incomplete.') parser_init.add_argument('-L', '--use-local-source', default=[], action='append', nargs=2, metavar=('SOURCE_NAME', 'PATH'), help='Symlink local source into a build, instead of getting it as prescribed by a configuration (useful for local development).') + parser_init.add_argument('--init-vscode', action=argparse.BooleanOptionalAction, default=bool(shutil.which('code')), + help='Generate VSCode workspace configuration (default: %(default)s)') parser_init.set_defaults(func=init_config) parser_status = subparsers.add_parser('status', help='Check if the setup needs to be synchronized with configuration') From patchwork Wed Mar 25 06:51:50 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: AdrianF X-Patchwork-Id: 84313 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id F1B46FEA820 for ; Wed, 25 Mar 2026 07:14:11 +0000 (UTC) Received: from mta-65-227.siemens.flowmailer.net (mta-65-227.siemens.flowmailer.net [185.136.65.227]) by mx.groups.io with SMTP id smtpd.msgproc01-g2.16763.1774422847650220442 for ; Wed, 25 Mar 2026 00:14:08 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=adrian.freihofer@siemens.com header.s=fm1 header.b=IJ9o22Yr; spf=pass (domain: rts-flowmailer.siemens.com, ip: 185.136.65.227, mailfrom: fm-1329275-20260325071405bc1495f6e300020713-bif_iq@rts-flowmailer.siemens.com) Received: by mta-65-227.siemens.flowmailer.net with ESMTPSA id 20260325071405bc1495f6e300020713 for ; Wed, 25 Mar 2026 08:14:05 +0100 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; s=fm1; d=siemens.com; i=adrian.freihofer@siemens.com; h=Date:From:Subject:To:Message-ID:MIME-Version:Content-Type:Content-Transfer-Encoding:Cc:References:In-Reply-To; bh=5slPa932EOIuwdVa2QjpcXBG0NCAeGQZyZ5bVhsu8sk=; b=IJ9o22YrnUuiOZ7yr8jwryPjNYeRkeLSBBj7Ri26LH8i7wzNi5p8WbzK+tWMLc32j3JlE4 s8dWwZV5wwYZHWtOfC8mUNNdlxY9k2ssM0f4UW+gzGKPl0BPwrlt+xfmj2CTRv/wamZ64V4k /wGN9A5vuzKuu6ICq9nmw6JlXq6lLw9RXhRleGbWTNXzi01NHS4RXl6mO38PD2njBsJbO3Z5 4p9eJj3Cj9vfC2eTyhjKYL04enZMVF6poUUDBuGJDYXqDdFCtYLIh+cadVbTFoO+3WGvWtS3 TB3TpEL66iIlHV+slifuVLcDoyCsK3+mkeFwX66Paww6ZJuik84aAzfw==; From: AdrianF To: bitbake-devel@lists.openembedded.org Cc: Adrian Freihofer Subject: [PATCH v2 08/10] tests/setup: add test_vscode for VSCode workspace generation Date: Wed, 25 Mar 2026 07:51:50 +0100 Message-ID: <20260325071342.47272-9-adrian.freihofer@siemens.com> In-Reply-To: <20260325071342.47272-1-adrian.freihofer@siemens.com> References: <20260325071342.47272-1-adrian.freihofer@siemens.com> MIME-Version: 1.0 X-Flowmailer-Platform: Siemens Feedback-ID: 519:519-1329275:519-21489:flowmailer List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Wed, 25 Mar 2026 07:14:11 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/bitbake-devel/message/19222 From: Adrian Freihofer Add test_vscode to BitbakeSetupTest covering the --init-vscode option introduced in 'bitbake-setup: generate config files for VSCode': - init --init-vscode creates bitbake.code-workspace with the expected top-level structure (folders, settings, extensions). - Folders list conf and each non-symlink git repo in layers/; all paths are relative. - Bitbake extension settings (pathToBuildFolder, pathToEnvScript, disableConfigModification) are set correctly. - File associations (*.conf, *.inc) and python.analysis.extraPaths / python.autoComplete.extraPaths are populated. - init --no-init-vscode does not create a workspace file. - update after a layer change preserves user-added folders and settings while updating managed ones. - update with a corrupt workspace file logs an error and leaves the file unchanged. Signed-off-by: Adrian Freihofer --- lib/bb/tests/setup.py | 93 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/lib/bb/tests/setup.py b/lib/bb/tests/setup.py index 1af3d8b50..52120546c 100644 --- a/lib/bb/tests/setup.py +++ b/lib/bb/tests/setup.py @@ -538,3 +538,96 @@ print("BBPATH is {{}}".format(os.environ["BBPATH"])) custom_setup_dir = 'special-setup-dir-with-cmdline-overrides' out = self.runbbsetup("init --non-interactive -L test-repo {} --setup-dir-name {} test-config-1 gadget".format(self.testrepopath, custom_setup_dir)) _check_local_sources(custom_setup_dir) + + def test_vscode(self): + if 'BBPATH' in os.environ: + del os.environ['BBPATH'] + os.chdir(self.tempdir) + + self.runbbsetup("settings set default registry 'git://{};protocol=file;branch=master;rev=master'".format(self.registrypath)) + self.add_file_to_testrepo('test-file', 'initial\n') + self.add_json_config_to_registry('test-config-1.conf.json', 'master', 'master') + + # --init-vscode should create bitbake.code-workspace + self.runbbsetup("init --non-interactive --init-vscode test-config-1 gadget") + setuppath = self.get_setup_path('test-config-1', 'gadget') + workspace_file = os.path.join(setuppath, 'bitbake.code-workspace') + self.assertTrue(os.path.exists(workspace_file), + "bitbake.code-workspace should be created with --init-vscode") + + with open(workspace_file) as f: + workspace = json.load(f) + + # top-level structure + self.assertIn('folders', workspace) + self.assertIn('settings', workspace) + self.assertIn('extensions', workspace) + self.assertIn('yocto-project.yocto-bitbake', + workspace['extensions']['recommendations']) + + # folders: conf dir + test-repo (symlinks like oe-init-build-env-dir are skipped) + folder_names = {f['name'] for f in workspace['folders']} + self.assertIn('conf', folder_names) + self.assertIn('test-repo', folder_names) + + # folder paths must be relative so the workspace is portable + for f in workspace['folders']: + self.assertFalse(os.path.isabs(f['path']), + "Folder path should be relative, got: {}".format(f['path'])) + + # bitbake extension settings + settings = workspace['settings'] + self.assertTrue(settings.get('bitbake.disableConfigModification')) + self.assertEqual(settings['bitbake.pathToBuildFolder'], + os.path.join(setuppath, 'build')) + self.assertEqual(settings['bitbake.pathToEnvScript'], + os.path.join(setuppath, 'build', 'init-build-env')) + + # file associations + self.assertIn('*.conf', settings.get('files.associations', {})) + self.assertIn('*.inc', settings.get('files.associations', {})) + + # python extra paths: test-repo/scripts/ exists and should be listed + extra_paths = settings.get('python.analysis.extraPaths', []) + self.assertTrue(any('scripts' in p for p in extra_paths), + "python.analysis.extraPaths should include the scripts dir") + self.assertEqual(settings.get('python.analysis.extraPaths'), + settings.get('python.autoComplete.extraPaths')) + + # --no-init-vscode should NOT create a workspace file + self.runbbsetup("init --non-interactive --no-init-vscode test-config-1 gadget-notemplate") + notemplate_path = self.get_setup_path('test-config-1', 'gadget-notemplate') + self.assertFalse( + os.path.exists(os.path.join(notemplate_path, 'bitbake.code-workspace')), + "bitbake.code-workspace should not be created with --no-init-vscode") + + # update with --init-vscode after a layer change should preserve + # user-added folders and settings while still rewriting managed ones + workspace['folders'].append({"name": "user-folder", "path": "user/custom"}) + workspace['settings']['my.user.setting'] = 'preserved' + with open(workspace_file, 'w') as f: + json.dump(workspace, f, indent=4) + + self.add_file_to_testrepo('test-file', 'updated\n') + os.environ['BBPATH'] = os.path.join(setuppath, 'build') + self.runbbsetup("update --update-bb-conf='no'") + del os.environ['BBPATH'] + + with open(workspace_file) as f: + updated = json.load(f) + self.assertIn('user/custom', {f['path'] for f in updated['folders']}, + "User-added folder was removed during update") + self.assertIn('my.user.setting', updated['settings'], + "User-added setting was removed during update") + + # update with a corrupt workspace file should log an error and leave it unchanged + self.add_file_to_testrepo('test-file', 'updated-again\n') + with open(workspace_file, 'w') as f: + f.write('{invalid json') + os.environ['BBPATH'] = os.path.join(setuppath, 'build') + self.runbbsetup("update --update-bb-conf='no'") + del os.environ['BBPATH'] + with open(workspace_file) as f: + content = f.read() + self.assertEqual(content, '{invalid json', + "Corrupt workspace file should not be modified") From patchwork Wed Mar 25 06:51:51 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: AdrianF X-Patchwork-Id: 84316 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 56CF9FEA822 for ; Wed, 25 Mar 2026 07:14:12 +0000 (UTC) Received: from mta-65-228.siemens.flowmailer.net (mta-65-228.siemens.flowmailer.net [185.136.65.228]) by mx.groups.io with SMTP id smtpd.msgproc01-g2.16760.1774422847649471384 for ; Wed, 25 Mar 2026 00:14:08 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=adrian.freihofer@siemens.com header.s=fm1 header.b=eo63SPfH; spf=pass (domain: rts-flowmailer.siemens.com, ip: 185.136.65.228, mailfrom: fm-1329275-2026032507140574482c20350002073e-jpfbsx@rts-flowmailer.siemens.com) Received: by mta-65-228.siemens.flowmailer.net with ESMTPSA id 2026032507140574482c20350002073e for ; Wed, 25 Mar 2026 08:14:05 +0100 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; s=fm1; d=siemens.com; i=adrian.freihofer@siemens.com; h=Date:From:Subject:To:Message-ID:MIME-Version:Content-Type:Content-Transfer-Encoding:Cc:References:In-Reply-To; bh=jNtCpr5FFCFklpndqR4ITvVKi67qimn+drWpM3s3Nag=; b=eo63SPfHIVR8r9H/9Bm6lJlpW7LuX4lY7ox/4aLd0QHaIoI4XCOMh0O/EywtjyHmgKvG5F 704kRjNIBIRegRlzyi646D8w73mLQSgoPFjpNyGLW68M/nWBxBM5B/uJFh/JBxegM864TRod FgP3kBmW6WlXBuht8XW9cThGaSvXKY7fcDEPrpoko6VIfwYUViCM7qaZTeInBLao0Lrsil6a sjWOsH8VF7iiCKnYIRp2ffC05fDJsC9pKlgv/4SZthbKMP86zZ8/mnUPS5W6D5J0wxZQdZwZ q0cHDS/TGKUsKM6BnJdtaUWo5GZrV7hLIGmf7YKcmQLOdagVkA0NB6fA==; From: AdrianF To: bitbake-devel@lists.openembedded.org Cc: Adrian Freihofer Subject: [PATCH v2 09/10] bitbake-setup: add --rebase-conflicts-strategy to the update command Date: Wed, 25 Mar 2026 07:51:51 +0100 Message-ID: <20260325071342.47272-10-adrian.freihofer@siemens.com> In-Reply-To: <20260325071342.47272-1-adrian.freihofer@siemens.com> References: <20260325071342.47272-1-adrian.freihofer@siemens.com> MIME-Version: 1.0 X-Flowmailer-Platform: Siemens Feedback-ID: 519:519-1329275:519-21489:flowmailer List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Wed, 25 Mar 2026 07:14:12 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/bitbake-devel/message/19223 From: Adrian Freihofer When unpack_update() raises a LocalModificationsError due to local modifications in a layer repository, the caller now has a choice of how to handle it, controlled by the new --rebase-conflicts-strategy option on the 'update' subcommand: - abort (default): re-raise the error so the update stops with a clear message that names the affected source and its path. The fetcher has already aborted the rebase and restored the checkout to its previous state. - backup: rename the directory to a timestamped -backup path to preserve local work, then re-clone from upstream via fetcher.unpack(). The strategy is threaded from the CLI argument through build_status() and update_build() down to checkout_layers(). Signed-off-by: Adrian Freihofer --- bin/bitbake-setup | 42 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/bin/bitbake-setup b/bin/bitbake-setup index bebf2e705..a5f33b794 100755 --- a/bin/bitbake-setup +++ b/bin/bitbake-setup @@ -166,7 +166,7 @@ def _get_remotes(r_remote): return remotes -def checkout_layers(layers, confdir, layerdir, d): +def checkout_layers(layers, confdir, layerdir, d, rebase_conflicts_strategy='abort'): def _checkout_git_remote(r_remote, repodir, layers_fixed_revisions): rev = r_remote['rev'] branch = r_remote.get('branch', None) @@ -182,7 +182,33 @@ def checkout_layers(layers, confdir, layerdir, d): else: src_uri = f"{fetchuri};protocol={prot};rev={rev};nobranch=1;destsuffix={repodir}" fetcher = bb.fetch.Fetch([src_uri], d) - do_fetch(fetcher, layerdir) + repodir_path = os.path.join(layerdir, repodir) + try: + do_fetch(fetcher, layerdir) + except bb.fetch2.LocalModificationsError as e: + if rebase_conflicts_strategy != 'backup': + e.msg += ("\nUse 'bitbake-setup update --rebase-conflicts-strategy=backup'" + " to automatically back up the directory and re-clone from upstream," + " or use 'bitbake-setup init -L %s /path/to/local/checkout'" + " to work with a local checkout instead." % r_name) + raise + backup_path = add_unique_timestamp_to_path(repodir_path + '-backup') + logger.warning( + "Source '{}' at {} has local modifications that prevent an in-place update.\n" + "Renaming it to {} to preserve your work, then re-cloning from upstream." + .format(r_name, repodir_path, backup_path)) + os.rename(repodir_path, backup_path) + fetcher.unpack(layerdir) + except bb.fetch2.UnpackError as e: + if rebase_conflicts_strategy != 'backup': + raise + backup_path = add_unique_timestamp_to_path(repodir_path + '-backup') + logger.warning( + "Source '{}' at {} could not be updated in place (rebase conflict).\n" + "Renaming it to {} to preserve your work, then re-cloning from upstream." + .format(r_name, repodir_path, backup_path)) + os.rename(repodir_path, backup_path) + fetcher.unpack(layerdir) urldata = fetcher.ud[src_uri] revision = urldata.revision layers_fixed_revisions[r_name]['git-remote']['rev'] = revision @@ -441,9 +467,9 @@ def merge_overrides_into_sources(sources, overrides): layers[k] = v return layers -def update_build(config, confdir, setupdir, layerdir, d, update_bb_conf="prompt", init_vscode=False): +def update_build(config, confdir, setupdir, layerdir, d, update_bb_conf="prompt", init_vscode=False, rebase_conflicts_strategy='abort'): layer_config = merge_overrides_into_sources(config["data"]["sources"], config["source-overrides"]["sources"]) - sources_fixed_revisions = checkout_layers(layer_config, confdir, layerdir, d) + sources_fixed_revisions = checkout_layers(layer_config, confdir, layerdir, d, rebase_conflicts_strategy=rebase_conflicts_strategy) bitbake_config = config["bitbake-config"] thisdir = os.path.dirname(config["path"]) if config["type"] == 'local' else None setup_bitbake_build(bitbake_config, layerdir, setupdir, thisdir, update_bb_conf, init_vscode) @@ -926,7 +952,7 @@ def build_status(top_dir, settings, args, d, update=False): logger.plain('\nConfiguration in {} has changed:\n{}'.format(setupdir, config_diff)) if update: update_build(new_upstream_config, confdir, setupdir, layerdir, d, - update_bb_conf=args.update_bb_conf) + update_bb_conf=args.update_bb_conf, rebase_conflicts_strategy=args.rebase_conflicts_strategy) else: bb.process.run('git -C {} restore config-upstream.json'.format(confdir)) return @@ -935,7 +961,7 @@ def build_status(top_dir, settings, args, d, update=False): if are_layers_changed(layer_config, layerdir, d): if update: update_build(current_upstream_config, confdir, setupdir, layerdir, - d, update_bb_conf=args.update_bb_conf) + d, update_bb_conf=args.update_bb_conf, rebase_conflicts_strategy=args.rebase_conflicts_strategy) return logger.plain("\nConfiguration in {} has not changed.".format(setupdir)) @@ -1256,6 +1282,10 @@ def main(): parser_update = subparsers.add_parser('update', help='Update a setup to be in sync with configuration') add_setup_dir_arg(parser_update) parser_update.add_argument('--update-bb-conf', choices=['prompt', 'yes', 'no'], default='prompt', help='Update bitbake configuration files (bblayers.conf, local.conf) (default: prompt)') + parser_update.add_argument('--rebase-conflicts-strategy', choices=['abort', 'backup'], default='abort', + help="What to do when a layer repository has local modifications that prevent " + "an in-place update: 'abort' (default) aborts with an error message; " + "'backup' renames the directory to a timestamped backup and re-clones from upstream.") parser_update.set_defaults(func=build_update) parser_install_buildtools = subparsers.add_parser('install-buildtools', help='Install buildtools which can help fulfil missing or incorrect dependencies on the host machine') From patchwork Wed Mar 25 06:51:52 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: AdrianF X-Patchwork-Id: 84318 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id 9DDC9FEA82A for ; Wed, 25 Mar 2026 07:14:12 +0000 (UTC) Received: from mta-64-226.siemens.flowmailer.net (mta-64-226.siemens.flowmailer.net [185.136.64.226]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.17084.1774422847652281921 for ; Wed, 25 Mar 2026 00:14:09 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=adrian.freihofer@siemens.com header.s=fm1 header.b=M2SU/ChF; spf=pass (domain: rts-flowmailer.siemens.com, ip: 185.136.64.226, mailfrom: fm-1329275-202603250714059c4c4d090f000207e2-mmlnka@rts-flowmailer.siemens.com) Received: by mta-64-226.siemens.flowmailer.net with ESMTPSA id 202603250714059c4c4d090f000207e2 for ; Wed, 25 Mar 2026 08:14:05 +0100 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; s=fm1; d=siemens.com; i=adrian.freihofer@siemens.com; h=Date:From:Subject:To:Message-ID:MIME-Version:Content-Type:Content-Transfer-Encoding:Cc:References:In-Reply-To; bh=SxdTkzbFSIdX9urCQo/OO9D2Af+E6EbUyfvBGOMHHeQ=; b=M2SU/ChFo2rVKX9QCLd+fUjblrTkK7h6LrYBJFbI39+jlVIuFFEOP9atrmDbG3QXLP3Wfi l7f1FvNXNePUvb92OcyBk0LgJZsr/2ly6TGxhDAurcqy+wpz29YV30cyS0Zlqm/atZFsBVML K5qUT9RvRT/rURpO5SEICOmQQPbVpstalQJ7gF8SLU+n0KFpKnmwrSZ9/ztI1SMglL4NKe3X SdTILw91deSc8Q6VNK528KCOklLfVRlXyj0RwIeJqdDQcVoOFvmiVPg/8YllVC/YfWdgpWdO vX2nXsHREjSHpjxnUAURSx4meQ+548WIJLU9B+lYeOK7Dr+7iOYLNfTw==; From: AdrianF To: bitbake-devel@lists.openembedded.org Cc: Adrian Freihofer Subject: [PATCH v2 10/10] tests/setup: add test_update_rebase_conflicts_strategy Date: Wed, 25 Mar 2026 07:51:52 +0100 Message-ID: <20260325071342.47272-11-adrian.freihofer@siemens.com> In-Reply-To: <20260325071342.47272-1-adrian.freihofer@siemens.com> References: <20260325071342.47272-1-adrian.freihofer@siemens.com> MIME-Version: 1.0 X-Flowmailer-Platform: Siemens Feedback-ID: 519:519-1329275:519-21489:flowmailer List-Id: X-Webhook-Received: from 45-33-107-173.ip.linodeusercontent.com [45.33.107.173] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Wed, 25 Mar 2026 07:14:12 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/bitbake-devel/message/19227 From: Adrian Freihofer Add a dedicated test for the --rebase-conflicts-strategy option introduced in 'bitbake-setup: add --rebase-conflicts-strategy to the update command'. Three scenarios are covered that are not exercised by the existing test_setup: 1. Uncommitted tracked-file change, default 'abort' strategy: update must fail with an error message that names the source and its path; no backup directory is created. 2. Same uncommitted change, 'backup' strategy: the layer directory is renamed to a timestamped backup, the layer is re-cloned from upstream, and the result is clean (upstream content, no local modifications). 3. Committed local change that conflicts with the next upstream commit, 'backup' strategy: instead of the hard rebase-conflict failure that occurs with the default strategy, the conflicted directory is backed up and re-cloned successfully. A small _count_layer_backups() helper is added to the class and reused across scenarios. Signed-off-by: Adrian Freihofer --- lib/bb/tests/setup.py | 88 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/lib/bb/tests/setup.py b/lib/bb/tests/setup.py index 52120546c..f22fb242f 100644 --- a/lib/bb/tests/setup.py +++ b/lib/bb/tests/setup.py @@ -631,3 +631,91 @@ print("BBPATH is {{}}".format(os.environ["BBPATH"])) content = f.read() self.assertEqual(content, '{invalid json', "Corrupt workspace file should not be modified") + + def _count_layer_backups(self, layers_path): + return len([f for f in os.listdir(layers_path) if 'backup' in f]) + + def test_update_rebase_conflicts_strategy(self): + """Test the --rebase-conflicts-strategy option for the update command. + + Covers three scenarios not exercised by test_setup: + 1. Uncommitted tracked-file change + default 'abort' strategy → clean error + message that names the source and its path, and hints at --rebase-conflicts-strategy=backup. + 2. Uncommitted tracked-file change + 'backup' strategy → directory is + renamed to a timestamped backup and the layer is re-cloned cleanly. + 3. Committed local change that would cause a rebase conflict + 'backup' + strategy → backup + re-clone instead of a hard failure. + """ + if 'BBPATH' in os.environ: + del os.environ['BBPATH'] + os.chdir(self.tempdir) + + self.runbbsetup("settings set default registry 'git://{};protocol=file;branch=master;rev=master'".format(self.registrypath)) + self.add_file_to_testrepo('test-file', 'initial\n') + self.add_json_config_to_registry('test-config-1.conf.json', 'master', 'master') + self.runbbsetup("init --non-interactive test-config-1 gadget") + + setuppath = self.get_setup_path('test-config-1', 'gadget') + layer_path = os.path.join(setuppath, 'layers', 'test-repo') + layers_path = os.path.join(setuppath, 'layers') + + # Scenario 1: uncommitted tracked change, default 'abort' strategy + # Advance upstream so an update is required. + self.add_file_to_testrepo('test-file', 'upstream-v2\n') + # Modify the same tracked file in the layer without committing. + with open(os.path.join(layer_path, 'test-file'), 'w') as f: + f.write('locally-modified\n') + + os.environ['BBPATH'] = os.path.join(setuppath, 'build') + with self.assertRaises(bb.process.ExecutionError) as ctx: + self.runbbsetup("update --update-bb-conf='no'") + self.assertIn('has uncommitted changes', str(ctx.exception)) + self.assertIn('--rebase-conflicts-strategy=backup', str(ctx.exception)) + # No backup directory must have been created. + self.assertEqual(self._count_layer_backups(layers_path), 0, + "abort strategy must not create any backup") + + # Scenario 2: same uncommitted change, 'backup' strategy + out = self.runbbsetup("update --update-bb-conf='no' --rebase-conflicts-strategy=backup") + # One backup directory must now exist. + self.assertEqual(self._count_layer_backups(layers_path), 1, + "backup strategy must create exactly one backup") + # The re-cloned layer must be clean and at the upstream revision. + with open(os.path.join(layer_path, 'test-file')) as f: + self.assertEqual(f.read(), 'upstream-v2\n', + "re-cloned layer must contain the upstream content") + status = self.git('status --porcelain', cwd=layer_path).strip() + self.assertEqual(status, '', + "re-cloned layer must have no local modifications") + del os.environ['BBPATH'] + + # Scenario 3: committed conflicting change, 'backup' strategy + # Re-initialise a fresh setup so we start from a clean state. + self.runbbsetup("init --non-interactive --setup-dir-name rebase-conflict-setup test-config-1 gadget") + conflict_setup = os.path.join(self.tempdir, 'bitbake-builds', 'rebase-conflict-setup') + conflict_layer = os.path.join(conflict_setup, 'layers', 'test-repo') + conflict_layers = os.path.join(conflict_setup, 'layers') + + # Commit a local change that touches the same file as the next upstream commit. + with open(os.path.join(conflict_layer, 'test-file'), 'w') as f: + f.write('conflicting-local\n') + self.git('add test-file', cwd=conflict_layer) + self.git('commit -m "Local conflicting change"', cwd=conflict_layer) + + # Advance upstream with a conflicting edit. + self.add_file_to_testrepo('test-file', 'conflicting-upstream\n') + + os.environ['BBPATH'] = os.path.join(conflict_setup, 'build') + # Default stop strategy must still fail with a conflict error. + with self.assertRaisesRegex(bb.process.ExecutionError, "Merge conflict in test-file"): + self.runbbsetup("update --update-bb-conf='no'") + self.assertEqual(self._count_layer_backups(conflict_layers), 0) + + # Backup strategy must succeed: backup the conflicted dir and re-clone. + self.runbbsetup("update --update-bb-conf='no' --rebase-conflicts-strategy=backup") + self.assertEqual(self._count_layer_backups(conflict_layers), 1, + "backup strategy must create exactly one backup after a conflict") + with open(os.path.join(conflict_layer, 'test-file')) as f: + self.assertEqual(f.read(), 'conflicting-upstream\n', + "re-cloned layer must contain the upstream content after conflict backup") + del os.environ['BBPATH']