From patchwork Fri Jul 17 14:34:22 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Trevor Woerner X-Patchwork-Id: 92741 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 227EDC4451B for ; Fri, 17 Jul 2026 14:34:56 +0000 (UTC) Received: from mail-qk1-f174.google.com (mail-qk1-f174.google.com [209.85.222.174]) by mx.groups.io with SMTP id smtpd.msgproc01-g2.14992.1784298891020188060 for ; Fri, 17 Jul 2026 07:34:51 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=@gmail.com header.s=20251104 header.b=UchItkwt; spf=pass (domain: gmail.com, ip: 209.85.222.174, mailfrom: twoerner@gmail.com) Received: by mail-qk1-f174.google.com with SMTP id af79cd13be357-92e7632b193so478646585a.2 for ; Fri, 17 Jul 2026 07:34:50 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1784298890; x=1784903690; 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=OSX/CKAOckLe/FXpAxijy0EGZL3d/0Hzt3IOmYSN+gk=; b=UchItkwtYpLk9/EtpYHiGTXAmyUQaa+G2xVZKauaHOBvRFohvMlYceOq/775GOG65U AP1e7HZBLFi0Yw1xjP/lrLLZ7ljdieqaBic9r0qPAEk4wBpfGKgFJO/OBA3Kz55fnLRZ GGrC+tuEqbCVwKIXP1YsLgFvTSe67hdIfBaMfEm6ahyyOAcZDWXLY5oBUVWn6/++BdS7 h1EW4XLDUl5Frg93gguPEkAVpk2B9E+dnegM5OrtPJLlws3IFZpj6pfJhB8sjiZHH+DS HCeFvUXDNqUVP01iWEbmOe4tBmeQLmiHPqYKEQFTEUwhEk2Q6Uy0AQDmPoSM5IEEHDAE Mr6Q== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1784298890; x=1784903690; 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=OSX/CKAOckLe/FXpAxijy0EGZL3d/0Hzt3IOmYSN+gk=; b=hkT6B1tjqN6rqSO2V1qz0aU99/6JG6X96Kj+I+sMZWENhRXAab0N+PMoPEFM8a8/TJ LX8l21Uug9B34DuNYqCHBcDAWP6bPqpV1zWZd9FZ54frNfcoq3MrxWL0wavKHpuHfPlg JtjWOdqhQHoHaAALnsny+4Wp0zV71S4iE0IrOv0v3XPsBNU3ahg1cLtWhPA8f/7AMsvc ERiQvNKuLc/lX4IjtMWjFvDnJDES4aR9vD1JB4JuFD8F4WIeb39eAs3iiXpGc1+zo7Uf /y9eHT3Pwu2fHY1Aw1E8csEaWxK2C7mZxT8a/ODhNPnOb1ZA717SkIFj5zDR+puMdq6P RmCQ== X-Gm-Message-State: AOJu0Yx7S/Cl+oKXRFDmowKtY/SCU/Xe1IwLPUJhk8s32aAQn83YHYNr Dyfv68DI0OMSHM9yGTF8igJ1txXyWuxiCC1vnJG9FBb8X3KNIFibv59L61i/vQ== X-Gm-Gg: AfdE7cmA4oIRDjK2DIB+MbZOwFOZr59kGYM3vRJJgkjodeiixWOZ8lKMODfFpwrxUjI 8VZyu3sfqwflNjFjj16O7ZpxdLRJbf/DMP9Xt9IvGwjcxtMCdDIewZNGDDvcnjWKSKpv2s+It0h 6JnivC4FwBxjnlmMEGqzmmNPcM3S1t5IEPqEov2Y2y2xpsyeDKE9V9Xturls7yLafo2e9dJ/8sB MLnbNnQ1BKo0ooX06EIamTP9YNuK9ZFRq6jKsQjbQ9O5Zv+rFoYZ5/i6eIQJ8Eg1GsOKSLjGSlo sHvEoAQTrvO+Gh9YXDuB+j38t4m9oN8QyKorr58Jj0A1469/heGZm1RRDS4O2/h/bfa7vYmu/Zv BpKmcCx5sY3Pxq/gjxIdjGpsS+uUCrWnXcTkpgHF9sC/B9jNm8d5bpCW/059aD7bLROpyzPOvmJ sQylc71l8+EqCqBw87StwEAopZRkzwRjB1qWOEpTJ7pb1D X-Received: by 2002:a05:620a:31aa:b0:930:9fdd:f4e4 with SMTP id af79cd13be357-930b3f346bamr339466785a.23.1784298889754; Fri, 17 Jul 2026 07:34:49 -0700 (PDT) Received: from localhost.localdomain (pppoe-209-91-167-254.vianet.ca. [209.91.167.254]) by smtp.gmail.com with ESMTPSA id af79cd13be357-930b5493cb0sm165208785a.42.2026.07.17.07.34.48 for (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Fri, 17 Jul 2026 07:34:48 -0700 (PDT) From: Trevor Woerner To: openembedded-core@lists.openembedded.org Subject: [PATCH v2 1/4] oe/path: fix bare `false` NameError in __realpath's isdir guard Date: Fri, 17 Jul 2026 10:34:22 -0400 Message-ID: <20260717143425.3521709-2-twoerner@gmail.com> X-Mailer: git-send-email 2.51.0 In-Reply-To: <20260717143425.3521709-1-twoerner@gmail.com> References: <20260717143425.3521709-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 ; Fri, 17 Jul 2026 14:34:56 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/241197 __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 --- changes in v2: - No change; carried over from v1. --- 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 Fri Jul 17 14:34:23 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Trevor Woerner X-Patchwork-Id: 92743 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 30FDEC44520 for ; Fri, 17 Jul 2026 14:34:56 +0000 (UTC) Received: from mail-qk1-f180.google.com (mail-qk1-f180.google.com [209.85.222.180]) by mx.groups.io with SMTP id smtpd.msgproc01-g2.14993.1784298892481495578 for ; Fri, 17 Jul 2026 07:34:52 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=@gmail.com header.s=20251104 header.b=kVwkrrQC; spf=pass (domain: gmail.com, ip: 209.85.222.180, mailfrom: twoerner@gmail.com) Received: by mail-qk1-f180.google.com with SMTP id af79cd13be357-92ea24a2dbfso625708685a.0 for ; Fri, 17 Jul 2026 07:34:52 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1784298891; x=1784903691; 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=TJvTCZs6V3X6XsazFwWpHMahVbQW8WDmV5GTHVC8kvI=; b=kVwkrrQCCX1C7yl2xFRTPcFuNQlfmQ925qdShD1/WvZOjlJ2sm9622aRlVAxOZN8lu nnySuCErQ8/JYUqLljMmuN3DBhcb7Z/PjSL1xdk1AjKda2HM/RBTZ4plzpk43cVUE7pG JFSQDFossq/Jj78ydlob48E3hGIgZvIwdrZ7JwyD7UDAM8I1Ooxin9TynrcyTEa2t1Ur 6JuPR3x1+a8c8XW5qbiXqFDF8f76X4UBm3CS/jvzDPYFPR/B9pq9m9Z2/3aELQdCsvls N3c8nkmusbKWF+6lioHKu3t1jsZDJmWqPZfljD8wxihDHQDTCsMZGxX5koGJH51lPKyB gm3g== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1784298891; x=1784903691; 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=TJvTCZs6V3X6XsazFwWpHMahVbQW8WDmV5GTHVC8kvI=; b=HtO7K+5EAMJU0AreFfWzA10TfNIbruJXzq8gKKOUF6gaWW50uX14H1hw1Tzo7BI7E5 CKmnlWwMNquMX+d7LnENgpYoArC9T/gkUisSIbBhRiysrvFF/Th3LR3fY9YM4NbepMvJ KsvPCMSegjUCzscrpgliFdY2DklvKaD1pUbgRNCmkUluyD7jLTbd44LqrIkxKQseu93Y zvPG6AqykMBM/ZNN0TI9H6ruErkExXBxmoriUJ2oAZKdNsA5rxEbrGxeDmD9jakPX3N+ Nw/qcK+00Mnqk+cjD5u67Z1cMmfXuXop4fhuSRQOoOYQfnP/JtMwF3aozKXwI8Pl94uH YU5w== X-Gm-Message-State: AOJu0YznRL45h2ecLV2V4LmttLvWYNCpGcYagvwiChq9bG2H8oeVpFFz doMYjRVGtECJM5WKEG9yJ9ojiobGTnjhrYd3aBqv7STJkJ/61KY3Lz2CPgzXjg== X-Gm-Gg: AfdE7ckFjRZZ0DXw5sd9xP/g/lkcix7STmfVOju6WiHQgk8s3tBx5vstT1xTh1+2k99 nzF1ykFWDS+2YYra3sqYu3+YL6jq7+b8MR6t1TqaN3OT7AVhecwSKt58/ajKDx91Nz9wzzZZF6T CoSclHTgO/C1At+5lMXLCw4lWJY6h8b7DFZc2lGzj6P0AYhU9N+OnwyWaYpynHY17sFhtZf/lyG 2KAFhzStiH8aoyd+yo6owBspP13D2K8ObRrCF69rJyXD5eeOWHffxIF0mbCx/069GWoB17sPB69 eq/J/Ks0EKyUSWPHhqLknqb7RRpPBPJpuOYer/gAFgG9m7fEc3zfzAzV5MjG33XzTZ86+VCWa3W pq0z/EkFicNQhIiQAWjNQ+ArER4+EVDeIKrXn9sW61HYbU5bL3cEAZHekqnZNSGzW0BBI/d3KGO cOtZhL2gtPmIWek6O/ZnhHW8pQMXITir/V9OF/FclL00qpOGZi/lBCMh0= X-Received: by 2002:a05:620a:6017:b0:92f:362:80a0 with SMTP id af79cd13be357-930b3eaab20mr314521285a.6.1784298891310; Fri, 17 Jul 2026 07:34:51 -0700 (PDT) Received: from localhost.localdomain (pppoe-209-91-167-254.vianet.ca. [209.91.167.254]) by smtp.gmail.com with ESMTPSA id af79cd13be357-930b5493cb0sm165208785a.42.2026.07.17.07.34.49 for (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Fri, 17 Jul 2026 07:34:50 -0700 (PDT) From: Trevor Woerner To: openembedded-core@lists.openembedded.org Subject: [PATCH v2 2/4] oe/path: don't glob-expand the destination in symlink(force=True) Date: Fri, 17 Jul 2026 10:34:23 -0400 Message-ID: <20260717143425.3521709-3-twoerner@gmail.com> X-Mailer: git-send-email 2.51.0 In-Reply-To: <20260717143425.3521709-1-twoerner@gmail.com> References: <20260717143425.3521709-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 ; Fri, 17 Jul 2026 14:34:56 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/241198 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 --- changes in v2: - No change; carried over from v1. --- 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 Fri Jul 17 14:34:24 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Trevor Woerner X-Patchwork-Id: 92742 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 3DCE3C44524 for ; Fri, 17 Jul 2026 14:34:56 +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.15067.1784298894286500418 for ; Fri, 17 Jul 2026 07:34:54 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=@gmail.com header.s=20251104 header.b=Jfl2uEzt; 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-8eeb4508f29so80460276d6.0 for ; Fri, 17 Jul 2026 07:34:54 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1784298893; x=1784903693; 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=loH9pS1TiHmIzEs4SEHCy+VsBuz3HopPeLckocxKE+Q=; b=Jfl2uEzttCz6R9gYnzyxlm8Z4iV1Dauiay2kK3kODqlwWHXLOM6BEGUWJqO0bmzntu OtzLVrjq9VAh4fw527AmE/QawT4YIC2tFZT0z6ht2ysLcIX4lZvhYDunTHWjuY6W8/Lg Rk5z+QuBehJeRvTLnq3O1WvD1CKuao/CXVERf4yf9cbDqcEv1ZMs6ncaKe3+8QtbVr6h u03vG+U5FXjjT8HIx7GAmpM5o716Tu8Dj7Z10dCsoI46uf0cQ3Ob6EcqGlKBtKdAZSrQ 3oLTGqS4P5sb89rj3x+bpt9U8CF4dkfoyQkxIVAsh7hTqtIDLliwQ0hkIScbsLPOc/xD UVaw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1784298893; x=1784903693; 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=loH9pS1TiHmIzEs4SEHCy+VsBuz3HopPeLckocxKE+Q=; b=s68DDmIX+5XB7ZlnjBCXNG0cMYcLm/CKlATlRpBLbhe5h0u+m+vyKJmcOVmL1cf2Wz hJwNF/Gvh+hxuzB5cnyXcaF/Nv5W02ikIS8tS9jSBJbUhogMIMJg197TPHpX203cgC2V 7RSoAt1VT2Q+ukaMI53w/1jgU+xFLEN6tsmgkozilpLuDorZbqkNz48mH6KUMjNV5xYY ysxs8oE5k9K8nIgD6/mu2vmWIYgsd06Awn2U3hI/28lriAlDza8PG0Kf/fuLMyiZV4bz X6KEgSJwza0lrvYynCGsnJxEZ6VldXoAE/xpJLNYo4rp4miNirKRtmUcYlQDilN/oYwR m2QA== X-Gm-Message-State: AOJu0YzT2qE/R0fsRNeC8OyWUr/+mCQs5FBekFPb77wCogZYrBArf3va WGRBBF7sWvb6QVGrPlZ7GHLaIiOP4574Bp1BqOVkpm4ajsd2UdYjjzlr3N0MfQ== X-Gm-Gg: AfdE7cl4iJOUFwNMMc7+W+SOAdcBTVqvvL5j0doq4S+yPplsvPz9DG144u8AbrcJn8y /6JjbnDHpasnMn5hGLCjczGXO4NASHl5IhgI5nNVGUtOCRTK8HTRJciQLQuHWr4+RsEnSExJzjE A+H/C9NKmuK3Ugr3VL/IYRcf8txDVl7rqQqf5jgS4X/VDvBJyLzV3AkdoU2cjLEqfR2oVEnMahy lP/cvoxcA7TNT9LhFMqkdA3t/IrjjqwgtleGZliTXL0b2qzEbqeWubt1yzsRDU7xLaBL/8/aNn6 nGPqgaSEFIg9CubIRrw91nUwXhp4ab44a5G+M1mXBEnvuW2Z6TUu0cWubflinuuNTNmKlTjhkw5 0goVvf9Ou7twWIneGijyGFfhxmqrmPUmlM6syeOwxmUFffJFu/lsmUz9eW6aEqs+JZQU3L65xhn HWjnoKrCI9UBRCVbiQ5U+yPU3oliafXitpPkPuLkDMpPQ/ X-Received: by 2002:a05:620a:2604:b0:914:e3dd:17ea with SMTP id af79cd13be357-930b3e9c2f5mr299988485a.12.1784298893049; Fri, 17 Jul 2026 07:34:53 -0700 (PDT) Received: from localhost.localdomain (pppoe-209-91-167-254.vianet.ca. [209.91.167.254]) by smtp.gmail.com with ESMTPSA id af79cd13be357-930b5493cb0sm165208785a.42.2026.07.17.07.34.51 for (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Fri, 17 Jul 2026 07:34:51 -0700 (PDT) From: Trevor Woerner To: openembedded-core@lists.openembedded.org Subject: [PATCH v2 3/4] oe/path: canonicalize('') should return '' rather than the cwd Date: Fri, 17 Jul 2026 10:34:24 -0400 Message-ID: <20260717143425.3521709-4-twoerner@gmail.com> X-Mailer: git-send-email 2.51.0 In-Reply-To: <20260717143425.3521709-1-twoerner@gmail.com> References: <20260717143425.3521709-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 ; Fri, 17 Jul 2026 14:34:56 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/241199 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 --- changes in v2: - No change; carried over from v1. --- 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 Fri Jul 17 14:34:25 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Trevor Woerner X-Patchwork-Id: 92744 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 36C2BC4451C for ; Fri, 17 Jul 2026 14:35:06 +0000 (UTC) Received: from mail-qk1-f170.google.com (mail-qk1-f170.google.com [209.85.222.170]) by mx.groups.io with SMTP id smtpd.msgproc01-g2.14995.1784298896867538265 for ; Fri, 17 Jul 2026 07:34:57 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=@gmail.com header.s=20251104 header.b=R7Ztto2u; spf=pass (domain: gmail.com, ip: 209.85.222.170, mailfrom: twoerner@gmail.com) Received: by mail-qk1-f170.google.com with SMTP id af79cd13be357-92e65e18969so162085085a.1 for ; Fri, 17 Jul 2026 07:34:56 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1784298896; x=1784903696; 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=vcjTyBwW+oDCCsa/C1SZVReIUk5fYwgvUrgtiIqo0to=; b=R7Ztto2u0EyyKrwTAQ/pYh2ek3iWG8hFL1OUQnpDqOrk2czlPDI04If7puU4MLmDEf 3Fkk03TYtqv0JEcLvPe4lx0uAHWq1XcGZg/AWg5CScRIA2lSK2pU2A7Ul1hw1G9CO7Bd dH/Li6wATFHrNKwcJHpeIjx1ZygD54BzugWQ3IV3Eje42Jt8VSP++hFbkKtMLdIFHGds 8bX7CQ6bdzBdIshe84WumhMJm5AY/uUzqwFg0DhS7SACtBW+J01stB1xrAzxwDCGBIcw dW4w/gLSarT05xw1al9ygSl9+AeJy5958Yh4mNLRzHPHSY/8uEb0BJv/gF5Lg/9NIChY p1Hg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1784298896; x=1784903696; 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=vcjTyBwW+oDCCsa/C1SZVReIUk5fYwgvUrgtiIqo0to=; b=R9ZQAizkHFn6niEo5poXkaVEwzrjI3H4ZL9XjO92FSMOzrTbACPJSPTiCIIHBBYF35 mvY1085d6StG/Qg6mRI6fpMMbFPuwkptqCjTyg7X7Q1VGdRYiXNeIQ+5YlxtwV4sC6Sm CVw7kMCwgJHW8+i5/oOgHrXPBmkb+cwN30xnwcTQM0IqFwy1aftWkSM215XwVWju8Jw9 33N7RDLJ6tvL8Iiu0VqQwBXqWLY7GAPBFiA37/Ec3eE5wOh67CpXBDbeSXyrQmsE/csa pOZMGOmQpDoC31eDq+pFjo6MHYx1zwauAehTL9rtfdMu/NcMjGUKuxhse8df5f5LRYSN jC2A== X-Gm-Message-State: AOJu0Ywl/G4c2yQlaXLTkV519mxtvljWAujYT6cYz7g07VthCcIHi1Kt ABtDqqYLf680MSGEyFaZtOZfu6Hs5OrVWK7JgxnPOwlXTgYfMWAOVnADKOgQIg== X-Gm-Gg: AfdE7cl0rLOD7/HzqHeVxPJMO8dTJudjQtJOpq7Jn2tFHjTFYpMYyg04zuXty3ufM6o OhBQt95TFawAUNC0DEDAHJUvlL/E/++3DcjpomXiXE92rB2i99hD1Py6RtHl6U4uh+qtXAdPTNN T7i+mFAfd+y6fi/Lz/f4kf93nHaINE0n5r0vdRlfv+X//HKtiBsgY7PFWnbN6Mz6OqvIKEJwQp5 x1GsJq11yjgjUyYFVevjLIbNlDIajy5FMuWzHndD9F/M65zSBEuWbHGx1XkKGnywAKpW7eujEGU jeKNLVuHlIKWa3Dr9NVyX2NMpvH2JB24Abd2ubfOJ+ExXtnDqrQtbpM+j/UXhEGhsd0ZBfPpoZM /MxvrQgIt6BVabfUfQadFOcSrTybojf9D6Tht3EDAcDPGSwADVFA+Fg4eEuTQmjCVHvm7zNjcIR KQLoFM5uCTPidgsr64cTFADUxql3FFCK5kpZe5kveuHAoK X-Received: by 2002:a05:620a:258c:b0:915:fc16:2ae0 with SMTP id af79cd13be357-930a5ff3dd2mr791761685a.25.1784298895129; Fri, 17 Jul 2026 07:34:55 -0700 (PDT) Received: from localhost.localdomain (pppoe-209-91-167-254.vianet.ca. [209.91.167.254]) by smtp.gmail.com with ESMTPSA id af79cd13be357-930b5493cb0sm165208785a.42.2026.07.17.07.34.53 for (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Fri, 17 Jul 2026 07:34:53 -0700 (PDT) From: Trevor Woerner To: openembedded-core@lists.openembedded.org Subject: [PATCH v2 4/4] oeqa/selftest/liboe: cover oe.path's own path logic Date: Fri, 17 Jul 2026 10:34:25 -0400 Message-ID: <20260717143425.3521709-5-twoerner@gmail.com> X-Mailer: git-send-email 2.51.0 In-Reply-To: <20260717143425.3521709-1-twoerner@gmail.com> References: <20260717143425.3521709-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 ; Fri, 17 Jul 2026 14:35:06 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-core/message/241200 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. realpath() also gets its harder contracts covered: a self-referential link and an A->B->A cycle both raise ELOOP rather than spinning, a symlink several components deep still resolves under use_physdir, and a missing trailing component raises ENOENT by default but is tolerated with assume_dir=True. symlink() is shown to stage a dangling link whose target does not exist yet, make_relative_symlink() is exercised several directories below its target so the multi-level "../" loop runs, and canonicalize() drops a run of "$" tokens and preserves a trailing slash. AI-Generated: codex/claude-opus 4.8 (xhigh) Signed-off-by: Trevor Woerner --- changes in v2: - Expand PathTests from 37 to 46 cases; no source changes. - realpath(): cover ELOOP on a self-referential link and on an A->B->A cycle, multi-component resolution under use_physdir, and the assume_dir ENOENT-vs-tolerated contract. - symlink(): cover a dangling link whose target does not exist yet. - make_relative_symlink(): cover a link several directories below its target (multi-level '../'). - canonicalize(): cover a run of '$' tokens all dropped, and a dropped '$' token before a trailing-slash path. --- meta/lib/oeqa/selftest/cases/liboe.py | 376 ++++++++++++++++++++++++++ 1 file changed, 376 insertions(+) diff --git a/meta/lib/oeqa/selftest/cases/liboe.py b/meta/lib/oeqa/selftest/cases/liboe.py index 930354c931a3..96969da1617f 100644 --- a/meta/lib/oeqa/selftest/cases/liboe.py +++ b/meta/lib/oeqa/selftest/cases/liboe.py @@ -7,7 +7,10 @@ from oeqa.selftest.case import OESelftestTestCase from oeqa.utils.commands import get_bb_var, get_bb_vars, bitbake, runCmd import oe.path +import errno import os +import tempfile +import unittest.mock class CopyTreeTests(OESelftestTestCase): @@ -135,3 +138,376 @@ 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) + + def test_symlink_creates_dangling_symlink(self): + # symlink() does not require the source to exist; callers rely on + # being able to stage a link before its target is populated. + src = os.path.join(self.tmp_path, "not_created_yet") + 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) + self.assertFalse(os.path.exists(dst)) # dangling: target is absent + + # --- 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") + + def test_make_relative_symlink_several_levels_deep_becomes_relative(self): + # The link sits several directories below the target, so the depth + # loop has to prepend more than one '../' segment. A single-level + # case never exercises that loop body. + target = os.path.join(self.tmp_path, "target.txt") + with open(target, "w") as f: + f.write("content") + deep = os.path.join(self.tmp_path, "a", "b", "c") + os.makedirs(deep) + link = os.path.join(deep, "link") + os.symlink(target, link) + oe.path.make_relative_symlink(link) + result = os.readlink(link) + self.assertFalse(os.path.isabs(result)) + self.assertEqual(result, "../../../target.txt") + 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) + "/") + + def test_canonicalize_multiple_variable_tokens_all_skipped(self): + # Every "$"-bearing token is dropped, so a list made only of them + # canonicalizes to the empty string (no separators, no cwd). + self.assertEqual(oe.path.canonicalize("$A,$B,$C"), "") + + def test_canonicalize_variable_and_trailing_slash_path(self): + # A dropped "$" token followed by a real path with a trailing slash: + # the slash is preserved and no phantom leading separator appears. + result = oe.path.canonicalize("$VAR,%s/" % 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_resolves_symlink_several_components_deep(self): + # With use_physdir=True realpath resolves each intermediate + # component, so a link buried under real subdirectories must still + # resolve to its final target. + os.makedirs(os.path.join(self.tmp_path, "a", "b", "c")) + link = os.path.join(self.tmp_path, "link_to_c") + os.symlink("a/b/c", link) + result = oe.path.realpath(os.path.join(link, "file"), + self.tmp_path, assume_dir=True) + self.assertEqual(result, + os.path.join(self.tmp_path, "a", "b", "c", "file")) + + def test_realpath_self_referential_symlink_raises_eloop(self): + # A link that points at itself must be reported as ELOOP rather than + # spinning until the recursion limit or the stack blows. + link = os.path.join(self.tmp_path, "loop") + os.symlink("loop", link) + with self.assertRaises(OSError) as cm: + oe.path.realpath(link, self.tmp_path) + self.assertEqual(cm.exception.errno, errno.ELOOP) + + def test_realpath_mutual_symlink_cycle_raises_eloop(self): + # An A -> B -> A cycle is the same contract as the self-loop. + a = os.path.join(self.tmp_path, "a") + b = os.path.join(self.tmp_path, "b") + os.symlink("b", a) + os.symlink("a", b) + with self.assertRaises(OSError) as cm: + oe.path.realpath(a, self.tmp_path) + self.assertEqual(cm.exception.errno, errno.ELOOP) + + def test_realpath_missing_component_raises_enoent_without_assume_dir(self): + # By default a missing path component is an error. + with self.assertRaises(OSError) as cm: + oe.path.realpath(os.path.join(self.tmp_path, "nope", "missing"), + self.tmp_path) + self.assertEqual(cm.exception.errno, errno.ENOENT) + + def test_realpath_missing_component_tolerated_with_assume_dir(self): + # assume_dir=True lets realpath resolve a path whose trailing + # components do not exist yet, returning the composed path. + result = oe.path.realpath( + os.path.join(self.tmp_path, "nope", "missing"), + self.tmp_path, assume_dir=True) + self.assertEqual(result, + os.path.join(self.tmp_path, "nope", "missing")) + + 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)