From patchwork Wed Jul 15 22:12:49 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Trevor Woerner X-Patchwork-Id: 92614 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 C212CC44507 for ; Wed, 15 Jul 2026 22:13:18 +0000 (UTC) Received: from mail-qv1-f51.google.com (mail-qv1-f51.google.com [209.85.219.51]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.530.1784153588758664350 for ; Wed, 15 Jul 2026 15:13:08 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=@gmail.com header.s=20251104 header.b=ozOPLhpP; spf=pass (domain: gmail.com, ip: 209.85.219.51, mailfrom: twoerner@gmail.com) Received: by mail-qv1-f51.google.com with SMTP id 6a1803df08f44-8ee6912d86dso20186616d6.1 for ; Wed, 15 Jul 2026 15:13:08 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1784153588; x=1784758388; darn=lists.openembedded.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:to:from:from:to:cc:subject:date:message-id :reply-to:content-type; bh=UHWyP6C9RO1cR0Q39T8hPasFwyTHgJx/1qizImZrLF0=; b=ozOPLhpPClRm7tcRDyGUABB/D1WH7rzR3Iox2Ixkca+pdVhIIUGK+gdcAJt/n4EYUJ kQp+gHLy4pn9m9ouTDvc93tTYokoskJJBcuDHag2kkkChdErGOv/L6t2weQ0T9cr1yQf lj0zXJrnqv8uQxZ8UM8pg7YpD7kyPvCeJjy5b6RZUQRyXxp/BQCv7nkcS5PTxIssUXV5 uHemOKE4qbL9oeguNnTFRiky58BPl/GTCaG9KEXq+mDjn3isTxkl+QJpb9gvXz0raom9 ZBR8+DG/Ew6iLOhthe6DVMtyTHlYXc0nznH4UkWqiHwWKKS7ydzS1KmkvTWflL6Rs5mi VZJw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1784153588; x=1784758388; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:to:from:x-gm-gg:x-gm-message-state:from:to :cc:subject:date:message-id:reply-to:content-type; bh=UHWyP6C9RO1cR0Q39T8hPasFwyTHgJx/1qizImZrLF0=; b=gv11y8nVQl/C4uxId1ZsIs+K9y2iDkBqNUhojDdBdridv4EbdOggyVeHvHa9ZBSoTd pdKBLim/eoocZpxx7/eaHMZrwtf4oHA58XnQyMpj/U1HyK21Ty3TANs5hvWBR3G1o7n5 R7RD9aFyWM607kHu+n9Uf3mphjuNTofBmk9vtgcq6n1CO4jtsROuGp9rAlr1KPqnE7Pk RSMAc1V0Dq398nkQ27EwVTxqit6gg9w4+zF7Bf1A7Cy9BisRC3Qs8cnQ0+owoEyQacBL iA6nKB4h97j7XKHcAdWezMGiIMyOzhoOb3gapJgxuLStHURYTPoQKHFZ5o9Bjh9jcflN cUAQ== X-Gm-Message-State: AOJu0YyzZOFYieh204KuOI5Yl1GIfoar97uDTXJPHnC0s1yexIg4b3DZ 23hV0U90ELbmBAOYtkvYpSYjNnRMw6kI/zGkB3fGGWHD/PbG1R1pgYcH7NsChg== X-Gm-Gg: AfdE7ckXsbFp6c/0lR07dO3Ppn1L+/r11G0iGv58mX99gsWU6FAS4YnaW8ojo+DHPpT HxvPhLY1aGSMlTmkLflYt82Lh/J9+Ug3qql40PEIKZA8aDiXtsTLGYkhIv0Wzz+40E7UoL2SnIV P7JRwzaKcjoXhTZZQY5P58GOG+cjElDQKXEzByOMQdbMIYk3SSakd8KdpN4zSffki76oHOuYwag 4u0+5wxgzCUF6K5qSz2CyeZWkIq85okm5BU1QPZJsSm8E8u1nWgnNHnGeENKwRlUR32rU5Ha9tT Rg2C8XqlXBAZErYsM9t2vj7g8mhLOsUZdZuyk7sRkorHBMljibkbM1U1FooiT0O41cTfWqTnFTs yKFZSnPtIDET+TP0ZcTFHltCEI+4rxzsj4e2fTubu/Ga5EzXZLVwEX+uoWu13/1XTHNP94ySP4a xJBew/Y4RvpTRbhDBiRItxWFZPnE5dbM4Nz0PwKsmQtrMA X-Received: by 2002:a05:6214:5e12:b0:8e9:f62b:8fa0 with SMTP id 6a1803df08f44-90401d69c74mr206382366d6.53.1784153587645; Wed, 15 Jul 2026 15:13:07 -0700 (PDT) Received: from localhost.localdomain (pppoe-209-91-167-254.vianet.ca. [209.91.167.254]) by smtp.gmail.com with ESMTPSA id 6a1803df08f44-8ffd50e081dsm204822086d6.9.2026.07.15.15.13.06 for (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 15 Jul 2026 15:13:06 -0700 (PDT) From: Trevor Woerner To: openembedded-core@lists.openembedded.org Subject: [PATCH 1/4] oe/path: fix bare `false` NameError in __realpath's isdir guard Date: Wed, 15 Jul 2026 18:12:49 -0400 Message-ID: <20260715221252.3369108-2-twoerner@gmail.com> X-Mailer: git-send-email 2.51.0 In-Reply-To: <20260715221252.3369108-1-twoerner@gmail.com> References: <20260715221252.3369108-1-twoerner@gmail.com> MIME-Version: 1.0 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, 15 Jul 2026 22:13:18 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/241044 __realpath() wraps its os.path.isdir() probe in a bare except that assigns `is_dir = false`. `false` is not a Python name, so when os.path.isdir() does raise (for example on an ELOOP path), the handler meant to absorb the error instead raises NameError and aborts the walk. Use the builtin False so the guard degrades to "not a directory" as intended. AI-Generated: codex/claude-opus 4.8 (xhigh) Signed-off-by: Trevor Woerner --- meta/lib/oe/path.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meta/lib/oe/path.py b/meta/lib/oe/path.py index c4588ad0e65a..f0462d276196 100644 --- a/meta/lib/oe/path.py +++ b/meta/lib/oe/path.py @@ -234,7 +234,7 @@ def __realpath(file, root, loop_cnt, assume_dir): try: is_dir = os.path.isdir(file) except: - is_dir = false + is_dir = False return (file, is_dir) From patchwork Wed Jul 15 22:12:50 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Trevor Woerner X-Patchwork-Id: 92616 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 F12D7C4450A for ; Wed, 15 Jul 2026 22:13:18 +0000 (UTC) Received: from mail-qv1-f43.google.com (mail-qv1-f43.google.com [209.85.219.43]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.531.1784153591731608540 for ; Wed, 15 Jul 2026 15:13:11 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=@gmail.com header.s=20251104 header.b=KN+tThNO; spf=pass (domain: gmail.com, ip: 209.85.219.43, mailfrom: twoerner@gmail.com) Received: by mail-qv1-f43.google.com with SMTP id 6a1803df08f44-8eeb4508f29so60990216d6.0 for ; Wed, 15 Jul 2026 15:13:11 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1784153591; x=1784758391; darn=lists.openembedded.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:to:from:from:to:cc:subject:date:message-id :reply-to:content-type; bh=4KqfHRkEdsSYR04nrc35sDThN0NPbY1cAjRHVsvXoXU=; b=KN+tThNO8vfMG+vX+F+uuF1Bx3KeigEsprcr7WfMy0H/enxh0mzkg5ozI8ENP5FkeU CFAGg3AM+R5Ptz40WUryNf3SeKxJJj2eMxIstdpNaro+80L/J22z3kzxx4/qNTK0SgGn SA8e9j0DpDRJ76UNi9XYxdHxHudxJlfGO+FwGQ3LNarPn+0rasOu9O/O9mEUlMZZ5C4N v6hIXP/Zj/2hHfXLOkPqFY14ksvkqTqrGYpjI6yaTSkG+2EbXE6wWNPU9CX32SoqyWQV EqcYFHUp5WOds4g0xO8Wgjh/YOiicCITHFuCNsz4jSsx7vqrt4DcRWmF2D+jc/R5r0d1 MAug== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1784153591; x=1784758391; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:to:from:x-gm-gg:x-gm-message-state:from:to :cc:subject:date:message-id:reply-to:content-type; bh=4KqfHRkEdsSYR04nrc35sDThN0NPbY1cAjRHVsvXoXU=; b=A1E6YZR0J5ZKlJ2xkgJ7IxBn9KJnkX+YD4W8kCyjhbQDcbUrOls9Z2S/T8PoEf/lRy dyThSIa3n8RSg8/3d0OTDAAXMOb/icQ3IzZX4lIPseZGXuBTMbEdpPlb82N1vdrIvT+5 YGf7nv99QLoG7XofD64JJmZ5Hop9ii8wI2y9UQJzdKIgY3ys9/YLWfKw+kV3M8MvbG1A xEOh+BUY7B8P09782VyuWHjgvRZ3iDzli49AUIWSeOSvGQCAY+E+52ylVwRUXx+NGlXi SDba4az3i4Zcf2XtaUm6pZjUiLT64BUpQq8JlikIL6IzkoxARKizVST5gHkYCikOEnVc gLhw== X-Gm-Message-State: AOJu0Yx6iPOEEVkpvma8kOaCy1eeRvJk49I+m9KPTcgj2R40BUJE+hXd RxyWBSRquliZiqXrlrpz80TXXcWXO2hestfoHBiaGI84vZbIXZCXqXiQBdH9hw== X-Gm-Gg: AfdE7cmZmlCAkisHmsWPWDXGoQql7J92hL7KE49a5LnK0EolC1Viszk6r5wGIdyXpfG Pm2Mku+ORIV+z4kJ65yXbUeI1r+Vfh/Nei07bf7tNL4pogr7zIGb1IFbFfulBqCgtan/Hg3uEEc oC8sq/M5mh074Ofg50MY6ALpl4vBlwTahNEA7DwQEi63NIyP1LaZFT/lEFMbhB1Sa4yuJ6giAvo zgvvRUL1SidhZ2zwIOUcm+8EW6PvLsM1PYPcg97sfhkbrOe7fUIseJLPrD/k4navNGh/evhCLES N4IPZNvmsddpqq0WiMNC1oNtgxoa5qpM2GKqzpRnu0z9MEy5nDSA7tgsWjfDiPMboYlZDX5BN2a VPXg8s69tGIz0hs5abZG/57+HuoEMetHnSvhFv+XaMBlZ30gravkCkDYA5YD9rCuOUbUqwmUpyr S8GQth5aL6btypGWmO2M3JmY+GMe5Zb0zHb96OPh1RgZZ2 X-Received: by 2002:a05:6214:2025:b0:907:31a6:c24f with SMTP id 6a1803df08f44-90758de66e0mr63425836d6.41.1784153590389; Wed, 15 Jul 2026 15:13:10 -0700 (PDT) Received: from localhost.localdomain (pppoe-209-91-167-254.vianet.ca. [209.91.167.254]) by smtp.gmail.com with ESMTPSA id 6a1803df08f44-8ffd50e081dsm204822086d6.9.2026.07.15.15.13.07 for (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 15 Jul 2026 15:13:08 -0700 (PDT) From: Trevor Woerner To: openembedded-core@lists.openembedded.org Subject: [PATCH 2/4] oe/path: don't glob-expand the destination in symlink(force=True) Date: Wed, 15 Jul 2026 18:12:50 -0400 Message-ID: <20260715221252.3369108-3-twoerner@gmail.com> X-Mailer: git-send-email 2.51.0 In-Reply-To: <20260715221252.3369108-1-twoerner@gmail.com> References: <20260715221252.3369108-1-twoerner@gmail.com> MIME-Version: 1.0 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, 15 Jul 2026 22:13:18 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/241045 symlink(source, destination, force=True) cleared an existing destination by calling remove(destination). remove() treats its argument as a glob pattern (it iterates glob.glob(path)), so a destination whose name contains glob metacharacters is mishandled: a name such as "foo[bar]" may fail to match itself and be left in place, or a pattern could match and delete unrelated files. Remove the literal destination instead: unlink it directly, and fall back to rmtree() for a directory, ignoring ENOENT. This keeps the force=True semantics without passing the path through glob. AI-Generated: codex/claude-opus 4.8 (xhigh) Signed-off-by: Trevor Woerner --- meta/lib/oe/path.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/meta/lib/oe/path.py b/meta/lib/oe/path.py index f0462d276196..44a9ff9d0757 100644 --- a/meta/lib/oe/path.py +++ b/meta/lib/oe/path.py @@ -169,7 +169,18 @@ def symlink(source, destination, force=False): """Create a symbolic link""" try: if force: - remove(destination) + # Remove the exact destination path. Do not route this through + # remove(), which treats its argument as a glob pattern: a + # destination containing glob metacharacters (for example a + # '[' in the name) could fail to match, or match and delete + # unrelated files. + try: + os.unlink(destination) + except OSError as exc: + if exc.errno == errno.EISDIR: + shutil.rmtree(destination) + elif exc.errno != errno.ENOENT: + raise os.symlink(source, destination) except OSError as e: if e.errno != errno.EEXIST or os.readlink(destination) != source: From patchwork Wed Jul 15 22:12:51 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Trevor Woerner X-Patchwork-Id: 92617 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 09FE7C44510 for ; Wed, 15 Jul 2026 22:13:19 +0000 (UTC) Received: from mail-qt1-f176.google.com (mail-qt1-f176.google.com [209.85.160.176]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.532.1784153593038975753 for ; Wed, 15 Jul 2026 15:13:13 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=@gmail.com header.s=20251104 header.b=NXBHgzc+; spf=pass (domain: gmail.com, ip: 209.85.160.176, mailfrom: twoerner@gmail.com) Received: by mail-qt1-f176.google.com with SMTP id d75a77b69052e-51bf2479349so50021941cf.2 for ; Wed, 15 Jul 2026 15:13:12 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1784153592; x=1784758392; darn=lists.openembedded.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:to:from:from:to:cc:subject:date:message-id :reply-to:content-type; bh=NyYa6f7hFHI8mU762ysNeDHWi/pTuaG5NGgybIb9+38=; b=NXBHgzc++9mXj6uLMSRjDpg/DfcI5AccCGN/e5QHfYCH/H3yZ0QwCLSnwYOepXsgkE W1hp61GQP8r1awStJVcdwRC1CSIW2VP0P2PHrtmHCdbHS+dfxqlSVk9VT39BSRsqMNXm sEejl5FJ4FwegB7/Y+01SAVleaJMPZgygml7g7okdMdgPKQj8PDjpDBo0tra5/ke1bJJ MwLvlMFME6pNssHkTOoBxZPODmdx9hsklIDcuqk7o7h0smCAKHHAk1qVpJeDRaX2m0wt QIA1Rzsb6hnB0XAAT+UQ61wjsK3bw7NciGJBogyfIs2SGY2XWH8HHPfOEWXMsQc5QRY2 E1Mw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1784153592; x=1784758392; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:to:from:x-gm-gg:x-gm-message-state:from:to :cc:subject:date:message-id:reply-to:content-type; bh=NyYa6f7hFHI8mU762ysNeDHWi/pTuaG5NGgybIb9+38=; b=lzD93bR8vBBvbw4IBHyUydKxjbLfyK+vKLiKrvvqyyWR+DfPPZ73+D0STlZZBgbH2e deASfQ6JaMMPC338wrEWY/s9Iko79/FMe7h0dNxJQXMwuJeZiKUv72MG/YfxVV7cQq2+ LVU4O4tBAwqNBDXh1+uIhg2TjVY6wBOohVzyPJrvxq4DH11rM6k2TagQhI68zSadIifJ FaQd1z1nekW8m2+nCs5nTOTWS59kx/GJ9WsyKsWLJNG78Y3GmbShVRFcILacx79FfyHg 9JKTxIthbTb0TQM6Z/xYEcKfW8ZuDgBEJQ6lFYnCywEkpL4JNjQGyCeRpY51BJJRpeF2 b68Q== X-Gm-Message-State: AOJu0YwexERMavYRfgNCcLIQDPqYJyKMnh2CUGN594WdA8CRHcmNUu3l 2I3t3Jlb5zpC2uW31tE0OmqKW4M0WePfbbdH1WgIcMyDwB4T8TmKoIhqpaftew== X-Gm-Gg: AfdE7cmQuS7W7Ls2di9d8+UcdPDsOrLpZ4DA3NuVGZAqVWoGit/hUYG63FX3GPSCdBG /yTbYly7LD71GOdEF7eWqftjWmUmma7mQSnYrCpWMdjW+AwpboWIpvx57HjitH9Nvvsjx2DEQLE P8M1EU0baY2FwZnTZOnxA2V3k+6a3jYuqrra43jKfyo7zZLKPdiFmyK/fFT4w+Vr8yNVdZ0EkLa 91v1xNeyGqBjlq9wuZzWDKukdEKaecJwuuIx+F+d8nEqQL8a95x4rKYBw1hZzQqP4Hd7RvIhpbJ NE2ebEYgoKIOTcCIIQkErM2tnmdW348dIP1nh1THp2XUYdA9TmU7G1mf6IooHD1GULfYpTghPMC fYxX9ZioI2YWBsRJRrlxEqGL3TmWvBKrpG3HTanpo7jDaGYrl2qNI8qvsnOu6hzumpqqIQk7vXq 3hb6psa8W5XjTXPYKIYQONt/eIWJIW4w7RtiJPDQEDdgcq X-Received: by 2002:a05:622a:190c:b0:516:d955:ea2 with SMTP id d75a77b69052e-51e4217e650mr83179851cf.0.1784153591591; Wed, 15 Jul 2026 15:13:11 -0700 (PDT) Received: from localhost.localdomain (pppoe-209-91-167-254.vianet.ca. [209.91.167.254]) by smtp.gmail.com with ESMTPSA id 6a1803df08f44-8ffd50e081dsm204822086d6.9.2026.07.15.15.13.10 for (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 15 Jul 2026 15:13:10 -0700 (PDT) From: Trevor Woerner To: openembedded-core@lists.openembedded.org Subject: [PATCH 3/4] oe/path: canonicalize('') should return '' rather than the cwd Date: Wed, 15 Jul 2026 18:12:51 -0400 Message-ID: <20260715221252.3369108-4-twoerner@gmail.com> X-Mailer: git-send-email 2.51.0 In-Reply-To: <20260715221252.3369108-1-twoerner@gmail.com> References: <20260715221252.3369108-1-twoerner@gmail.com> MIME-Version: 1.0 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, 15 Jul 2026 22:13:19 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/241046 canonicalize() splits its input on the separator and runs each token through os.path.realpath(). os.path.realpath('') returns the current working directory, so canonicalize('') and canonicalize(None) wrongly produced the cwd instead of an empty string, and a stray separator (for example "a,,b") injected a spurious cwd entry into the result. Skip empty tokens alongside the existing unexpanded-variable skip so only real paths are canonicalized. AI-Generated: codex/claude-opus 4.8 (xhigh) Signed-off-by: Trevor Woerner --- meta/lib/oe/path.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/meta/lib/oe/path.py b/meta/lib/oe/path.py index 44a9ff9d0757..7255d99bc2f7 100644 --- a/meta/lib/oe/path.py +++ b/meta/lib/oe/path.py @@ -356,7 +356,11 @@ def canonicalize(paths, sep=','): # prefixes in sting compares later on, where the slashes then are important. canonical_paths = [] for path in (paths or '').split(sep): - if '$' not in path: + # Skip empty tokens as well as unexpanded bitbake variables: an + # empty path would otherwise reach os.path.realpath(''), which + # returns the current working directory, so canonicalize('') or + # canonicalize(None) would wrongly produce the cwd instead of ''. + if path and '$' not in path: trailing_slash = path.endswith('/') and '/' or '' canonical_paths.append(os.path.realpath(path) + trailing_slash) From patchwork Wed Jul 15 22:12:52 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Trevor Woerner X-Patchwork-Id: 92615 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 C3410C44501 for ; Wed, 15 Jul 2026 22:13:18 +0000 (UTC) Received: from mail-qv1-f51.google.com (mail-qv1-f51.google.com [209.85.219.51]) by mx.groups.io with SMTP id smtpd.msgproc02-g2.534.1784153596233389512 for ; Wed, 15 Jul 2026 15:13:16 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=@gmail.com header.s=20251104 header.b=KsVieEqy; spf=pass (domain: gmail.com, ip: 209.85.219.51, mailfrom: twoerner@gmail.com) Received: by mail-qv1-f51.google.com with SMTP id 6a1803df08f44-8eefd0c5f59so27425146d6.3 for ; Wed, 15 Jul 2026 15:13:16 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1784153595; x=1784758395; darn=lists.openembedded.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:to:from:from:to:cc:subject:date:message-id :reply-to:content-type; bh=mbPIPjG2kHq3UehXAVCNJVKosdIgHZQiGn9FQYVfAlc=; b=KsVieEqyqTLwrm9oDznnvkrcgIYo7i/D4kBVoBbdqlt3fh4lsQSnuYK1Hh0falRpHd O0wZsIUqN+RWW+aeNIUXXbBXGXWdDaCLG0qgBIZgH42Z/D3dXl9be0cSoIeWJrDMWs2i 9eZro9dv3gKqP3+TQO9uzREPkqpQ9DMRwI8ZB/Xox7cKQ9lKlPfi3HGh2JDPr4xSKaYM unVFqsb8y/PHoHouwTsG5/t6RgIov28b2VTteebqAKGCCDzKYUCOnLOe3VSjWiB0BGev DIoUepgXpLXRVJ3qWQbBhwXs+kCmH7NlvQh1P0GZ3arzcMWEjFA6BZ+8P/rMHlVYse6T iqiw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1784153595; x=1784758395; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:to:from:x-gm-gg:x-gm-message-state:from:to :cc:subject:date:message-id:reply-to:content-type; bh=mbPIPjG2kHq3UehXAVCNJVKosdIgHZQiGn9FQYVfAlc=; b=mlH6A+jOfZR2oSrEKyGvwDrbTOVJGr1I1bs6QyXGCMoyrrpAzEnTGrT9OePyVd6r4O rvjxMD2itbBW006IPZPEMxdidkAPWhKgARsSIuS0BdcR+GGfcEoX1O+v9yCfZYhZRZ+b nxMNGWwUoCoIjp155WMdPDLRuprqgbFBuEH1UgWKXf8h0gf8087nvIdH0SiO2gwW8Gk3 ypsWsyBWQdfVUHGAaMxFIHj/4iS+hOsm6a8u46K+iS1n6+S/K9/QjByC70rsnEvvasO9 qj/Bw24ZFGibETxq1WFgbK+E6GGO+aPglu9aDVR52DPpGgKUF0HtdSvA7uDdfv82cT2A RCsg== X-Gm-Message-State: AOJu0YyZW6tg2W17bo6Ic7LPWhtSvzLHBG4wDt63IZins6j93U6aMI0W fFlG95jPWDI6kaEDWIP6aaZW8qpezBrvIXCbsZTDsnQ/f8LMLRib9OL/G95e0A== X-Gm-Gg: AfdE7cnhSYjWqR8CrsWbFvRGk5NaHmPq5rwldbScW2ZcT7QSkiCwH7oFRMXLDLKYLTB /TQ5jnwQOgL3TA1rcLrLAJRfkRvsEkisIXPEQpJ4E6JGCrsTZNURZKUiEasgJXyM1uooP9vjqAd MhnC1MiOkYNnjt7pLhCdSHfySXheQEmJaktKROwsd5WjB0VqONB9WfK4CRBLGQnRff+HWeMM1FO j3dkU/k5mafVIhnasid3avyufM1XoDjQjwF0KDBRALp9ojmQZlTjkxm0hbc3Ey9IZG/nEjOe5n7 NbKxbQmyaXn4OPj75Sg2eQ6yz988Mukv9FpDkjoJVMEHqZWm8gnvl/jZh8Jo7ooVkYVnsjUIydU pNfDPxpAiFAlTp2HhczhHZwMs9b5jGssQD3C9PYdkL0GVj7krz3Mt0PJv0zEb9ysOD96mqPSigz +iXFciRB3Py+Nanuq2VjnntIAlvs/pJ7s/JZ/oDKWYiLYg X-Received: by 2002:a05:6214:23c6:b0:8e9:d24:c21f with SMTP id 6a1803df08f44-903ffc6db8cmr230334036d6.14.1784153594813; Wed, 15 Jul 2026 15:13:14 -0700 (PDT) Received: from localhost.localdomain (pppoe-209-91-167-254.vianet.ca. [209.91.167.254]) by smtp.gmail.com with ESMTPSA id 6a1803df08f44-8ffd50e081dsm204822086d6.9.2026.07.15.15.13.11 for (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 15 Jul 2026 15:13:12 -0700 (PDT) From: Trevor Woerner To: openembedded-core@lists.openembedded.org Subject: [PATCH 4/4] oeqa/selftest/liboe: cover oe.path's own path logic Date: Wed, 15 Jul 2026 18:12:52 -0400 Message-ID: <20260715221252.3369108-5-twoerner@gmail.com> X-Mailer: git-send-email 2.51.0 In-Reply-To: <20260715221252.3369108-1-twoerner@gmail.com> References: <20260715221252.3369108-1-twoerner@gmail.com> MIME-Version: 1.0 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, 15 Jul 2026 22:13:18 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/241047 Add in-process coverage for oe.path's join(), is_path_parent(), symlink(), make_relative_symlink(), canonicalize(), which_wild() and realpath(), alongside the existing copytree tests. The cases pin behaviour that is easy to regress: __realpath() falls back to "not a directory" when os.path.isdir() raises; symlink(force=True) replaces the literal destination even when its name contains glob metacharacters, and never removes a pattern sibling; and canonicalize() skips empty tokens so '', None, and a stray separator do not inject the current working directory. AI-Generated: codex/claude-opus 4.8 (xhigh) Signed-off-by: Trevor Woerner --- meta/lib/oeqa/selftest/cases/liboe.py | 289 ++++++++++++++++++++++++++ 1 file changed, 289 insertions(+) diff --git a/meta/lib/oeqa/selftest/cases/liboe.py b/meta/lib/oeqa/selftest/cases/liboe.py index 930354c931a3..fa4902610cc4 100644 --- a/meta/lib/oeqa/selftest/cases/liboe.py +++ b/meta/lib/oeqa/selftest/cases/liboe.py @@ -8,6 +8,8 @@ from oeqa.selftest.case import OESelftestTestCase from oeqa.utils.commands import get_bb_var, get_bb_vars, bitbake, runCmd import oe.path import os +import tempfile +import unittest.mock class CopyTreeTests(OESelftestTestCase): @@ -135,3 +137,290 @@ exit 42 self.assertIn("exit status 42", string) self.assertIn("Standard Output: Via stdout", string) self.assertIn("Standard Error: Via stderr", string) + +class PathTests(OESelftestTestCase): + """ + In-process coverage for oe.path's own path logic: join, is_path_parent, + symlink, make_relative_symlink, canonicalize, which_wild and realpath. + Thin standard-library wrappers (relative -> os.path.relpath, + find -> os.walk) are left to the standard library. + """ + + def setUp(self): + super().setUp() + self._tmp = tempfile.TemporaryDirectory(prefix="oeqa-oepath-") + self.addCleanup(self._tmp.cleanup) + self.tmp_path = self._tmp.name + + # --- join -------------------------------------------------------------- + # join() is os.path.normpath("/".join(paths)); unlike os.path.join it + # does not treat an absolute right-hand component specially. + + def test_join_two_paths(self): + self.assertEqual(oe.path.join("a", "b"), "a/b") + + def test_join_absolute_rhs_is_not_special(self): + # os.path.join("a", "/b") == "/b"; oe.path.join() keeps it relative. + self.assertEqual(oe.path.join("a", "/b"), "a/b") + + def test_join_redundant_separators_normalised(self): + self.assertEqual(oe.path.join("a//", "b"), "a/b") + + def test_join_pardir_normalised(self): + self.assertEqual(oe.path.join("a", "..", "b"), "b") + + def test_join_empty_leading_component_is_absolute(self): + # "/".join(["", "b"]) is "/b", which normpath leaves absolute. + self.assertEqual(oe.path.join("", "b"), "/b") + + # --- is_path_parent ---------------------------------------------------- + + def test_is_path_parent_direct_child(self): + self.assertTrue(oe.path.is_path_parent("/usr", "/usr/bin")) + + def test_is_path_parent_unrelated_path(self): + self.assertFalse(oe.path.is_path_parent("/usr", "/tmp")) + + def test_is_path_parent_child_is_not_parent_of_its_parent(self): + self.assertFalse(oe.path.is_path_parent("/usr/bin", "/usr")) + + def test_is_path_parent_prefix_string_is_not_a_path_parent(self): + # "/usrlocal" shares a string prefix with "/usr" but is not below it; + # the trailing-separator handling must reject it. + self.assertFalse(oe.path.is_path_parent("/usr", "/usrlocal")) + + def test_is_path_parent_no_paths_returns_false(self): + self.assertFalse(oe.path.is_path_parent("/usr")) + + def test_is_path_parent_all_paths_must_be_below(self): + self.assertTrue(oe.path.is_path_parent("/usr", "/usr/bin", "/usr/lib")) + + def test_is_path_parent_one_path_outside_fails(self): + self.assertFalse(oe.path.is_path_parent("/usr", "/usr/bin", "/tmp")) + + # --- symlink ----------------------------------------------------------- + + def test_symlink_creates_symlink(self): + src = os.path.join(self.tmp_path, "target.txt") + with open(src, "w") as f: + f.write("content") + dst = os.path.join(self.tmp_path, "link") + oe.path.symlink(src, dst) + self.assertTrue(os.path.islink(dst)) + self.assertEqual(os.readlink(dst), src) + + def test_symlink_matching_existing_is_idempotent(self): + src = os.path.join(self.tmp_path, "target.txt") + with open(src, "w") as f: + f.write("content") + dst = os.path.join(self.tmp_path, "link") + oe.path.symlink(src, dst) + oe.path.symlink(src, dst) + self.assertEqual(os.readlink(dst), src) + + def test_symlink_conflicting_existing_raises(self): + src1 = os.path.join(self.tmp_path, "t1.txt") + src2 = os.path.join(self.tmp_path, "t2.txt") + for p in (src1, src2): + with open(p, "w") as f: + f.write(p) + dst = os.path.join(self.tmp_path, "link") + oe.path.symlink(src1, dst) + with self.assertRaises(OSError): + oe.path.symlink(src2, dst) + + def test_symlink_force_overwrites_existing_symlink(self): + src1 = os.path.join(self.tmp_path, "t1") + src2 = os.path.join(self.tmp_path, "t2") + for p in (src1, src2): + with open(p, "w") as f: + f.write(p) + dst = os.path.join(self.tmp_path, "link") + oe.path.symlink(src1, dst) + oe.path.symlink(src2, dst, force=True) + self.assertEqual(os.readlink(dst), src2) + + def test_symlink_force_replaces_destination_with_glob_metacharacters(self): + # A destination whose name contains glob metacharacters must still be + # replaced. Routing the removal through remove() (which globs) fails + # to match "link[1]" against itself, leaving the stale entry in place. + src = os.path.join(self.tmp_path, "target") + with open(src, "w") as f: + f.write("content") + dst = os.path.join(self.tmp_path, "link[1]") + with open(dst, "w") as f: + f.write("stale regular file") + oe.path.symlink(src, dst, force=True) + self.assertTrue(os.path.islink(dst)) + self.assertEqual(os.readlink(dst), src) + + def test_symlink_force_does_not_delete_glob_siblings(self): + # The destination "keep?.txt" does not exist, but a sibling + # "keepX.txt" matches it as a glob pattern. force=True must remove + # only the literal destination, never a pattern sibling. + sibling = os.path.join(self.tmp_path, "keepX.txt") + with open(sibling, "w") as f: + f.write("do not delete me") + src = os.path.join(self.tmp_path, "target") + with open(src, "w") as f: + f.write("content") + dst = os.path.join(self.tmp_path, "keep?.txt") + oe.path.symlink(src, dst, force=True) + self.assertTrue(os.path.exists(sibling)) + self.assertEqual(os.readlink(dst), src) + + def test_symlink_force_replaces_existing_directory(self): + # An existing directory at the destination is torn down (EISDIR -> + # rmtree) before the link is created. + src = os.path.join(self.tmp_path, "target") + with open(src, "w") as f: + f.write("content") + dst = os.path.join(self.tmp_path, "dir") + os.mkdir(dst) + with open(os.path.join(dst, "child"), "w") as f: + f.write("x") + oe.path.symlink(src, dst, force=True) + self.assertTrue(os.path.islink(dst)) + self.assertEqual(os.readlink(dst), src) + + # --- make_relative_symlink --------------------------------------------- + + def test_make_relative_symlink_non_symlink_is_ignored(self): + regular = os.path.join(self.tmp_path, "file.txt") + with open(regular, "w") as f: + f.write("content") + oe.path.make_relative_symlink(regular) + self.assertTrue(os.path.isfile(regular)) + + def test_make_relative_symlink_already_relative_unchanged(self): + target = os.path.join(self.tmp_path, "target.txt") + with open(target, "w") as f: + f.write("content") + link = os.path.join(self.tmp_path, "link") + os.symlink("target.txt", link) + oe.path.make_relative_symlink(link) + self.assertEqual(os.readlink(link), "target.txt") + + def test_make_relative_symlink_absolute_becomes_relative_and_resolves(self): + target = os.path.join(self.tmp_path, "target.txt") + with open(target, "w") as f: + f.write("content") + link = os.path.join(self.tmp_path, "link") + os.symlink(target, link) + self.assertTrue(os.path.isabs(os.readlink(link))) + oe.path.make_relative_symlink(link) + result = os.readlink(link) + self.assertFalse(os.path.isabs(result)) + self.assertTrue(os.path.exists(link)) + with open(link) as f: + self.assertEqual(f.read(), "content") + + # --- canonicalize ------------------------------------------------------ + + def test_canonicalize_real_path_returned(self): + self.assertEqual(oe.path.canonicalize(self.tmp_path), + os.path.realpath(self.tmp_path)) + + def test_canonicalize_unexpanded_variable_is_skipped(self): + self.assertEqual(oe.path.canonicalize("$SOME_VAR"), "") + + def test_canonicalize_variable_token_dropped_leaving_real_path(self): + result = oe.path.canonicalize("$VAR," + self.tmp_path) + # The "$VAR" token is dropped entirely: no empty placeholder, no + # leading separator, just the canonical real path. + self.assertEqual(result, os.path.realpath(self.tmp_path)) + + def test_canonicalize_empty_string_returns_empty_string(self): + # os.path.realpath("") is the cwd; canonicalize("") must not leak it. + self.assertEqual(oe.path.canonicalize(""), "") + + def test_canonicalize_none_returns_empty_string(self): + self.assertEqual(oe.path.canonicalize(None), "") + + def test_canonicalize_empty_token_between_paths_is_dropped(self): + p1 = os.path.join(self.tmp_path, "a") + p2 = os.path.join(self.tmp_path, "b") + os.mkdir(p1) + os.mkdir(p2) + # The stray separator in "a,,b" must not inject a cwd entry. + result = oe.path.canonicalize("%s,,%s" % (p1, p2)) + self.assertEqual(result, "%s,%s" % (os.path.realpath(p1), + os.path.realpath(p2))) + + def test_canonicalize_trailing_slash_preserved(self): + result = oe.path.canonicalize(self.tmp_path + "/") + self.assertEqual(result, os.path.realpath(self.tmp_path) + "/") + + # --- which_wild -------------------------------------------------------- + + def test_which_wild_missing_tool_returns_empty(self): + self.assertEqual(oe.path.which_wild("totally_nonexistent_tool_xyz"), []) + + def test_which_wild_explicit_search_path(self): + tool = os.path.join(self.tmp_path, "mytool") + with open(tool, "w") as f: + f.write("#!/bin/sh\n") + os.chmod(tool, 0o755) + self.assertEqual(oe.path.which_wild("mytool", path=self.tmp_path), + [tool]) + + def test_which_wild_wildcard_pattern(self): + for name in ("foo-a", "foo-b", "bar"): + with open(os.path.join(self.tmp_path, name), "w") as f: + f.write("") + results = oe.path.which_wild("foo-*", path=self.tmp_path) + self.assertEqual(sorted(os.path.basename(p) for p in results), + ["foo-a", "foo-b"]) + + def test_which_wild_first_match_per_name_wins(self): + # A name found in an earlier PATH element shadows the same name later; + # reverse=True walks PATH the other way, so the other copy wins. + first = os.path.join(self.tmp_path, "first") + second = os.path.join(self.tmp_path, "second") + os.mkdir(first) + os.mkdir(second) + for d in (first, second): + with open(os.path.join(d, "tool"), "w") as f: + f.write("") + search = "%s:%s" % (first, second) + self.assertEqual(oe.path.which_wild("tool", path=search), + [os.path.join(first, "tool")]) + self.assertEqual(oe.path.which_wild("tool", path=search, reverse=True), + [os.path.join(second, "tool")]) + + # --- realpath ---------------------------------------------------------- + + def test_realpath_resolves_symlink_below_root(self): + target = os.path.join(self.tmp_path, "real") + os.mkdir(target) + link = os.path.join(self.tmp_path, "lnk") + os.symlink("real", link) + self.assertEqual(oe.path.realpath(link, self.tmp_path), target) + + def test_realpath_plain_path_is_returned(self): + sub = os.path.join(self.tmp_path, "d") + os.mkdir(sub) + self.assertEqual(oe.path.realpath(sub, self.tmp_path), sub) + + def test_realpath_path_outside_root_raises(self): + root = os.path.join(self.tmp_path, "root") + outside = os.path.join(self.tmp_path, "outside") + os.mkdir(root) + os.mkdir(outside) + with self.assertRaises(OSError): + oe.path.realpath(outside, root) + + def test_realpath_isdir_failure_falls_back_without_nameerror(self): + # The final stanza of __realpath tolerates any failure from + # os.path.isdir by treating the path as not-a-directory. Force isdir + # to raise and confirm the fallback returns the resolved path rather + # than raising NameError from an undefined fallback value. + sub = os.path.join(self.tmp_path, "d") + os.mkdir(sub) + + def boom(_path): + raise OSError("synthetic stat failure") + + with unittest.mock.patch("os.path.isdir", boom): + self.assertEqual( + oe.path.realpath(sub, self.tmp_path, use_physdir=False), sub)